mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 20:09:04 +00:00
33e7c1a819
Installed for Copilot with `gh skill install` and manually created a symlink for Claude to access all Copilot skills.
101 lines
4.1 KiB
Markdown
101 lines
4.1 KiB
Markdown
# 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`.
|