An actor protects its isolated state from simultaneous unsafe access. It does not freeze that state for an entire async method. When an operation awaits, other actor work can run before it resumes. A value checked before suspension may therefore be outdated afterward.
The concurrency lesson introduces actors and cancellation. This guide focuses on business invariants across suspension and on rejecting obsolete asynchronous results. Compiler-enforced isolation prevents a category of data races; the application still decides whether a response belongs to the latest request or whether an external effect needs reconciliation.
Await splits an operation into state-sensitive segments
Code between suspension points can update actor-owned state without another actor operation interleaving within that synchronous segment. At await, the operation can suspend and the actor can process other work. Do not assume a balance, selection or request identifier remains unchanged simply because the method began inside an actor.
A reservation that checks availability, awaits payment and then deducts stock needs a deliberate policy. It might reserve local state before waiting, revalidate afterward, or let an authoritative service handle the transaction. Each choice has failure and cancellation consequences. Merely placing the fields inside an actor does not decide the correct transaction semantics.
Keep short state transitions together when possible, and take snapshots for external work. After waiting, compare the snapshot's identity or version against current state before applying a result. This preserves isolation while making freshness a visible rule instead of an implicit scheduling assumption.
Cancellation saves work but does not prove freshness
Task cancellation is cooperative. Check Task.checkCancellation or Task.isCancelled at useful boundaries, and let cancellation-aware APIs stop their work. A CPU loop without checks may continue, and an external callback API may need an explicit cancellation adapter.
Cancelling the previous search can reduce unnecessary work, but a stale-result guard is still useful because cancellation might arrive after a response or the underlying operation might not honor it promptly. The latest-request token expresses the publication rule independently of how efficiently old work stops.
Do not convert cancellation into a successful empty result unless that is an explicit contract. A cancelled screen request should normally stop publishing. An external write that already happened remains an external write; cancelling its local task does not roll it back. Define idempotency or reconciliation where the domain needs stronger guarantees.
Keep task lifetime with the component owner
An unstructured Task should still have an owner that retains its handle and cancels it when appropriate. A view model can cancel previous work when a new request begins or when the component is disposed. Detached tasks should not be used as a shortcut to escape isolation diagnostics or lifecycle responsibility.
MainActor is appropriate for UI-owned state, while another actor can own domain state. Moving between these domains should transfer suitable snapshots rather than pass arbitrary mutable references. A generation guard belongs with the state being published so all publication paths use the same rule.
Use controlled tests to force the old request to finish after the new one. A fake service with explicit continuations or signals is more reliable than sleeping and hoping the scheduler follows a desired order. Test cancellation before completion, completion after cancellation and successful current publication as separate cases.
Define freshness for failures as well as successful values. An obsolete request's error should not replace the current request's successful status with an unrelated warning. Pass the same ticket through success and failure publication paths, and let the owner decide whether either belongs to the current generation. This keeps loading indicators, messages and results aligned instead of protecting only the final data field while stale errors remain visible.
Official references
Read the actor proposal's reentrancy discussion and the Swift concurrency guide. Review every await for assumptions about mutable state, then identify who owns cancellation and who decides whether a result is current. Those are concrete policies that an actor declaration alone cannot supply.
Swift Actor Reentrancy and Cancellation Examples
Each example shows the code, the output it produces and what each part does.
A versioned result publication boundary
The demonstration controls the order explicitly rather than rely on guessed network delays. A real search operation records its token before fetching, then attempts publication after fetching. The actor combines the freshness check and update in one synchronous operation, so another begin cannot slip between them.
actor SearchStore {
private var generation = 0
private var text = ""
func begin() -> Int {
generation += 1
return generation
}
func publish(_ result: String, for token: Int) -> Bool {
guard token == generation else { return false }
text = result
return true
}
func current() -> String { text }
}
@main struct Demo {
static func main() async {
let store = SearchStore()
let old = await store.begin()
let latest = await store.begin()
print(await store.publish("old result", for: old))
print(await store.publish("new result", for: latest))
print(await store.current())
}
}false
true
new resultSwift Actor Reentrancy and Cancellation in Real Projects
Worked exercise: Reject a result after replacement
Create a small actor that issues tickets and stores only results belonging to the current ticket. Represent a worker's result as an immutable Sendable value. The test sequence simulates the order a reentrant asynchronous workflow can create without depending on real networking.
struct SearchResult: Sendable { let label: String }
actor LatestResult {
private var ticket = 0
private var result: SearchResult?
func start() -> Int { ticket += 1; return ticket }
func complete(_ value: SearchResult, ticket candidate: Int) -> Bool {
guard candidate == ticket else { return false }
result = value
return true
}
func label() -> String { result?.label ?? "waiting" }
}
@main struct Demo {
static func main() async {
let owner = LatestResult()
let first = await owner.start()
let second = await owner.start()
let accepted = await owner.complete(SearchResult(label: "Book"), ticket: second)
let rejected = await owner.complete(SearchResult(label: "Pen"), ticket: first)
print(accepted)
print(rejected)
print(await owner.label())
}
}true
false
BookCompile a standalone async entry point with swiftc -parse-as-library Demo.swift. The bounded example does not approach integer overflow; a long-lived production counter needs an appropriate token or overflow policy. A UUID or another identity scheme can be suitable when monotonic arithmetic is unnecessary.
Swift Actor Reentrancy and Cancellation FAQ
Does an actor execute one async method completely before another?
Not necessarily. Actor isolation governs access to state, but an async method can suspend at await and allow other actor work before it resumes. Its earlier observations can then be stale. Keep invariant-preserving synchronous transitions together and revalidate after suspension when required. Treat the method as a sequence of segments with possible state changes between them, rather than as an automatic database-style transaction covering every external wait.
Why keep a generation guard if I cancel old tasks?
Cancellation is cooperative and can race with completion. An old task may finish just before cancellation, or a callback source may ignore the stop request until later. A generation guard states which result is allowed to update current state independently of that timing. Use cancellation to reduce wasted work and the guard to enforce publication freshness. Combining both addresses resource use and correctness without pretending that either mechanism alone solves every lifecycle case.
Can cancellation undo a remote update?
No automatic rollback is implied. The server may already have committed an update even when the local task stops waiting or loses its response. Use the service's transaction, idempotency or reconciliation contract to determine the final state. Keep this separate from actor isolation, which controls local state access. A correct local cancellation path can still require a later refresh or status query to learn what happened externally.
How should I test an out-of-order response?
Use a fake source with controllable completion signals. Start the old request, start the replacement, complete the replacement first and the old request afterward, then verify that current state still contains the replacement. Avoid relying on arbitrary sleeps that only sometimes create that order. Also test cancellation and errors independently. The important assertion concerns the publication policy, not the scheduler's incidental timing or the number of log messages printed.
Swift Actor Reentrancy and Cancellation Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Which test reliably checks stale response rejection?
Swift Actor Reentrancy and Cancellation Quiz
2 questions to check what you learned.
What can happen while an actor method is suspended at await?
- Other actor work can run and change isolated state.
- Every external effect is rolled back.
- All actor state is permanently frozen.
What should a latest-result guard combine in one actor operation?
- Cancelling every unrelated task globally.
- Checking the ticket and updating the current result.
- Only printing the ticket.