Swift Concurrency
Swift Concurrency Bugs That Pass Code Review
The concurrency mistakes that look reasonable in a pull request but fail under scrolling, cancellation, reentrancy or stale user intent.
4 min read
The code looks modern
Swift concurrency can make broken code look clean. The old callback pyramid is gone. The function reads top to bottom. There is an async method, an await, a Task, maybe an actor. In review, it looks like progress.
The problem is that concurrency bugs rarely announce themselves in the diff. They appear when a cell is reused, a view disappears, two requests return out of order, the app backgrounds during work, or an actor suspends between reading state and writing state.
Senior code review has to look past modern syntax and ask what can happen over time.
Bug one: work tied to the wrong lifetime
A common mistake is starting work from a view and assuming the work should live as long as the view. That is true for some tasks and false for others. Image loading for a disappearing cell should usually die. Saving a draft should not die because the user navigated back.
The review question is: what owns this work? If the work is visual, local and disposable, tying it to view lifetime may be correct. If the work represents user intent, data integrity or money, it needs a more durable owner and probably a persisted operation model.
This is why blanket advice fails. Cancel more is not always right. Never cancel is not right either. The right answer follows the meaning of the work.
Bug two: stale responses winning
Search is the clean example. The user types quickly. Several requests leave the app. The request for an older query returns after the newer one. If the app displays the older result, every individual request succeeded and the feature still failed.
The fix may be cancellation, request identifiers, checking the latest query before applying results, or modeling the search stream differently. The important habit is to review async code by asking whether the result still belongs to the current user intent.
That same bug appears in filters, profile screens, pagination, product detail pages and any flow where navigation or input changes faster than the network.
Bug three: actor reentrancy breaking an invariant
Actors isolate mutable state, but they do not remove the need to think. If an actor method reads state, awaits something, then writes based on the old read, another call may have entered during the suspension and changed the world.
The code can look perfectly reasonable. The actor is there. The compiler is happy. The bug is in the invariant, not the syntax.
In review, ask what must remain true across the await. If the answer matters, snapshot the needed value, re-check after suspension, split the operation, or move the mutation so the invariant is protected. The goal is not to use actors. The goal is to preserve the business rule.
Bug four: MainActor as a hiding place
Marking a type MainActor can make compiler errors disappear. It can also drag too much work onto the UI lane. Parsing, image processing, persistence and large transformations do not become cheap because the annotation is convenient.
A better review asks what truly belongs to UI isolation. View state, presentation decisions and UI-bound mutations often do. Heavy work usually does not. The boundary should make ownership clearer, not make the app more sluggish.
If a MainActor type is doing networking, mapping, caching and rendering decisions, the problem may not be concurrency at all. It may be an object with too many jobs.
What a good review asks
For every async change, ask five questions. What owns this work? Can it be cancelled? Should it survive navigation? Can an older result overwrite a newer intent? What state can change across each await?
Those questions catch more real bugs than arguing about whether async/await is cleaner than callbacks. Syntax is the visible part. Lifetime, ordering and ownership are where the defects hide.
Where this fits
This essay belongs to the Pass Interviews path: Swift, SwiftUI, concurrency, architecture, mobile system design, live coding and behavioral answer systems.