Files
Doug 33e7c1a819 Add Swift Concurrency Pro skill to the project. (#5662)
Installed for Copilot with `gh skill install` and manually created a symlink for Claude to access all Copilot skills.
2026-05-29 12:47:48 +01:00

101 lines
4.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Bug patterns
Real concurrency failure modes that LLMs produce frequently, with the preferred fix for each.
## Actor reentrancy: check-then-act across `await`
**Failure:** Actor method checks state, awaits, then acts on the stale check. Other callers may have mutated state during the suspension.
```swift
// BUG: Two callers can both see nil and both download.
// The force unwrap can crash if a third caller clears the cache mid-flight.
actor Cache {
var data: [String: Data] = [:]
func load(_ key: String) async throws -> Data {
if data[key] == nil {
data[key] = try await download(key)
}
return data[key]!
}
}
```
**Fix:** Capture the async result into a local before writing. For deduplication, store in-flight `Task` handles. See `actors.md` for the full pattern.
## Continuation resumed zero times
**Failure:** A `withCheckedThrowingContinuation` callback never fires (object deallocated, network timeout with no callback, early return before registering the handler, etc). The caller hangs forever.
**Fix:** Audit every code path to confirm the continuation is resumed. If the underlying API can silently drop the callback, add a timeout or restructure so the caller isn't left waiting. Always use `withCheckedThrowingContinuation` (not the unsafe variant) so that missed resumes are easier to diagnose.
## Continuation resumed twice
**Failure:** Two callbacks (e.g., a success handler and a cancellation handler) both resume the same continuation. `CheckedContinuation` traps at runtime; `UnsafeContinuation` causes undefined behavior.
**Fix:** Restructure the callback wiring so only one path can reach the continuation. If that isn't possible, guard with a `Bool` flag or use an `actor` to serialize access. Always default to `CheckedContinuation` so double resumes surface immediately during development and testing.
## Unstructured tasks in a loop
**Failure:** `for item in items { Task { await process(item) } }` creates fire-and-forget tasks with no cancellation propagation, no error collection, and no way to await completion.
**Fix:** Use `withTaskGroup` or `withThrowingTaskGroup`. See `structured.md`.
## Swallowed errors in Task closures
**Failure:** `Task { try await riskyWork() }` – if `riskyWork` throws, the error is silently lost. The user sees nothing; the operation just doesn't happen.
**Fix:** Handle the error inside the closure – show an alert, log to a visible surface, or propagate via a `@State` error property.
```swift
Task {
do {
try await riskyWork()
} catch {
self.errorMessage = error.localizedDescription
}
}
```
## Blocking the main actor with synchronous work
**Failure:** CPU-intensive work runs on `@MainActor` (or inside `Task {}` called from `@MainActor`), causing UI freezes. In Swift 6.2 this is more likely because `nonisolated` async functions now stay on the caller's executor by default.
**Fix:** Move the expensive work into an explicitly offloaded function using `@concurrent`, or use `Task.detached` as a last resort.
## Unbounded AsyncStream buffer
**Failure:** A high-throughput producer yields values faster than the consumer processes them. With the default `.unbounded` buffering policy, memory grows without limit.
**Fix:** Specify `.bufferingNewest(n)` or `.bufferingOldest(n)`. See `async-streams.md`.
## Ignoring `CancellationError` in catch blocks
**Failure:** A `catch` block retries or shows an error alert for `CancellationError`, which is a normal lifecycle event (e.g., user navigated away).
**Fix:** Check for cancellation before handling other errors:
```swift
do {
try await loadData()
} catch is CancellationError {
// Normal – view disappeared or task was cancelled. Do nothing.
} catch {
self.errorMessage = error.localizedDescription
}
```
## `@unchecked Sendable` hiding real races
**Failure:** A class is marked `@unchecked Sendable` to suppress compiler errors, but its mutable `var` properties have no synchronization. The data race still exists at runtime.
**Fix:** Restructure to use value types, use an `actor`, or move state behind a lock. See `bridging.md`.