01 · INTRODUCTION
What Is Actor Reentrancy in Swift?
The short answer
Actor reentrancy means that when an actor-isolated function suspends, the actor can execute other eligible work before that function resumes.
CODE EXAMPLE
actor LaunchStore {
private var selectedLaunchID: String?
private let api: LaunchAPI
init(api: LaunchAPI) {
self.api = api
}
func select(_ id: String) async throws {
selectedLaunchID = id
let details = try await api.fetchDetails(for: id)
// The actor was protected while this line ran,
// but selectedLaunchID may have changed during await.
print(selectedLaunchID as Any)
print(details)
}
}
The actor never allows two pieces of actor-isolated code to access its state at the same time. However, the call to select(_:) does not reserve the actor until the network request finishes. If it suspends at await, another call can enter the actor and change selectedLaunchID.
The central rule
Actor state is protected from simultaneous access, but it is not guaranteed to remain unchanged across an await.
02 · TUTORIAL
Why Actor Reentrancy Matters
Actors are often introduced with a useful sentence: an actor allows only one task at a time to access its isolated mutable state. That protection prevents data races, but it is easy to stretch the sentence too far.
It does not mean one async actor method runs from its first line to its last line before the actor considers any other work.
CODE EXAMPLE
func select(_ id: String) async throws {
selectedLaunchID = id // isolated work
let details = try await api.fetchDetails(for: id)
// possible suspension
cachedDetails = details // isolated work resumes later
}
The code before and after await executes in the actor's isolation domain. The suspension point creates an opportunity for another job to run on that actor between those two portions.
Without this idea, developers may believe that actor methods are automatically atomic transactions. They are not.
03 · TUTORIAL
What Actor Isolation Actually Guarantees
An actor protects its isolated state by serializing actor-isolated work. At any instant, the actor executes no more than one isolated portion of code.
CODE EXAMPLE
actor LaunchCounter {
private var count = 0
func increment() {
count += 1
}
func currentValue() -> Int {
count
}
}
Each synchronous method runs as one uninterrupted actor-isolated operation. Another task cannot enter the same actor halfway through count += 1 and observe a temporary state.
The guarantee can be stated precisely:
• only code isolated to this actor can directly access its mutable isolated state;
• the actor does not execute two actor-isolated portions simultaneously;
• synchronous actor code runs without actor interleaving because it has no suspension point.
Actor isolation protects access. It does not promise that a sequence containing suspension points is one indivisible operation.
04 · TUTORIAL
An Async Actor Method Is Divided at Suspension Points
A helpful model is to view an async actor method as a sequence of actor jobs separated by possible suspension points:
CODE EXAMPLE
actor LaunchStore {
private var status = "Idle"
private let api: LaunchAPI
init(api: LaunchAPI) {
self.api = api
}
func refresh() async throws {
status = "Loading" // Job 1
let launches = try await api.fetchUpcoming()
// possible suspension
status = "Loaded \(launches.count)" // Job 2
}
}
Conceptual model:
EXECUTION DIAGRAM
refresh() enters LaunchStore
│
▼
Job 1: status = "Loading"
│
▼
await api.fetchUpcoming()
│
├─ if the call suspends, refresh() leaves the actor executor
│
├─ other LaunchStore jobs may run
│
▼
Job 2: refresh() becomes eligible to resume
│
▼
status = "Loaded ..."
This is a teaching model, not a promise about the runtime's internal queue layout. The important observable rule is that other actor-isolated work may run during the suspension.
Important Terminology
Interleaving means separate actor jobs take turns making progress. They do not access the actor's isolated state simultaneously, but their operations can be woven together at suspension boundaries.
05 · TUTORIAL
Follow Two Calls Through One Actor
Suppose the user selects launch A and then quickly selects launch B. Both calls target the same actor:
CODE EXAMPLE
async let first: Void = store.select("launch-a")
async let second: Void = store.select("launch-b")
_ = try await (first, second)
One possible interleaving is:
CODE EXAMPLE
LaunchStore actor
1. select("launch-a") enters
2. selectedLaunchID = "launch-a"
3. request A suspends at await
4. select("launch-b") enters
5. selectedLaunchID = "launch-b"
6. request B suspends at await
7. request B resumes
8. B's details are stored
9. request A resumes
10. A's older details are stored last
This ordering is only an example; Swift does not guarantee which call enters or resumes first. Correctness must not depend on one scheduling order.
At no point did both calls execute actor-isolated state access simultaneously. Nevertheless, the older request can overwrite the newer selection. Actor isolation has done its job, but the feature's higher-level rule—show details for the latest selection—has not been enforced.
06 · TUTORIAL
State Can Change While Your Function Is Suspended
Consider the unsafe implementation:
CODE EXAMPLE
actor LaunchStore {
private var selectedLaunchID: String?
private var cachedDetails: LaunchDetails?
private let api: LaunchAPI
init(api: LaunchAPI) {
self.api = api
}
func select(_ id: String) async throws {
selectedLaunchID = id
let details = try await api.fetchDetails(for: id)
cachedDetails = details
}
}
Before the await, the function establishes an assumption: the current selection is id. After resuming, it stores the downloaded details without checking whether that assumption is still true.
Every await should therefore prompt one question:
Does the code after this await rely on actor state that I read or changed before it?
If the answer is yes, re-read or validate that state after the suspension before committing a result.
07 · TUTORIAL
Reentrancy Is Not a Data Race
A data race requires unsynchronized overlapping access to memory, with at least one write. Actor isolation prevents that kind of simultaneous access to the actor's isolated state.
Reentrant interleaving is different:
Problem
Data race
What happens?
Memory is accessed simultaneously without valid synchronization
Does actor isolation prevent it?
Yes, for actor-isolated state
Problem
Reentrant interleaving
What happens?
Actor jobs execute one at a time but in an unwanted logical order
Does actor isolation prevent it?
No
The stale launch result is data-race-free Swift. Each state access is isolated correctly. The error is in the ordering assumptions made by the feature.
The next article will give this broader category its own name: a logical race. For now, the important point is that “protected from data races” does not mean “every possible ordering produces the intended result.”
08 · TUTORIAL
Why Swift Actors Are Reentrant
Imagine that an actor refused all other work until every async method returned. A slow method waiting for a network response could make unrelated actor operations wait even though it was not currently using the actor.
CODE EXAMPLE
actor LaunchStore {
func refresh() async throws {
let launches = try await api.fetchUpcoming()
cache = launches
}
func cachedCount() -> Int {
cache.count
}
}
While refresh() is suspended on the network, reentrancy allows the actor to answer cachedCount() or begin another eligible operation.
This design:
• avoids unnecessarily blocking an actor during long waits;
• allows other useful work to make progress;
• helps avoid deadlock patterns involving actors that await one another;
• gives the runtime more scheduling flexibility.
Reentrancy is not a hole in actor isolation. It is part of the actor model's progress and scheduling design, with the trade-off that invariants must not be carried blindly across suspension points.
09 · TUTORIAL
await Marks a Possible Suspension Point
An await does not guarantee that the current task will suspend. The awaited operation may already be able to complete without suspension.
Source code must still treat the boundary as a place where suspension and interleaving are possible:
CODE EXAMPLE
let expectedID = selectedLaunchID
let details = try await api.fetchDetails(for: id)
// Re-read actor state. Do not assume it still equals expectedID.
guard selectedLaunchID == expectedID else {
return
}
cachedDetails = details
This is why Swift requires await to be visible in source. It tells the reader that execution can pause there and that state observed before the call may no longer describe the world afterward.
Do not test reentrancy by assuming a particular delay or print order. Scheduling is nondeterministic, and an operation that suspends today may complete immediately under different conditions.
10 · TUTORIAL
Design Safely Across Reentrant Boundaries
Capture the request's identity
Store enough information to identify the operation before suspending:
CODE EXAMPLE
requestGeneration += 1
let generation = requestGeneration
let details = try await api.fetchDetails(for: id)
Validate after resuming
Before changing actor state, confirm that the operation is still current:
CODE EXAMPLE
guard generation == requestGeneration else {
return
}
cachedDetails = details
A generation value handles repeated requests for the same launch as well as requests for different launches. A simple identifier comparison may be insufficient when two refreshes target the same ID.
Keep invariants inside synchronous segments
If several properties must change together, update them without placing an await between the mutations:
CODE EXAMPLE
private func commit(
_ details: LaunchDetails,
for id: String
) {
selectedLaunchID = id
cachedDetails = details
lastUpdated = .now
}
Because commit(_:for:) is synchronous and actor-isolated, another actor job cannot observe the properties halfway through this update.
Avoid artificial async methods
If an actor operation does not need to await anything, keep it synchronous. Callers outside the actor will still use await to cross into the actor, but the method body remains one uninterrupted isolated segment.
11 · TUTORIAL
A Complete Launch-Feature Example
This store accepts overlapping selections but commits only the newest request:
CODE EXAMPLE
actor LaunchDetailsStore {
enum State: Sendable {
case idle
case loading(id: String)
case loaded(LaunchDetails)
case failed(id: String, message: String)
}
private let api: LaunchAPI
private var generation = 0
private var state: State = .idle
init(api: LaunchAPI) {
self.api = api
}
func select(_ id: String) async {
generation += 1
let requestGeneration = generation
state = .loading(id: id)
do {
let details = try await api.fetchDetails(for: id)
guard requestGeneration == generation else {
return
}
state = .loaded(details)
} catch {
guard requestGeneration == generation else {
return
}
state = .failed(
id: id,
message: String(describing: error)
)
}
}
func snapshot() -> State {
state
}
}
The actor provides data-race safety for generation and state. The generation check provides the feature rule: only the latest selection may change the visible state.
The function deliberately checks the generation on both the success and failure paths. An old request should not replace a newer result, and its old error should not replace a newer loading or loaded state.
This implementation does not depend on which request finishes first. Every isolated segment remains serial, and each resumed operation validates the assumption it made before suspension.
12 · TUTORIAL
Common Misunderstandings
“Actors run every method to completion before starting another.”
Synchronous actor methods do. An async actor method can suspend, allowing other actor jobs to execute before it resumes.
“Reentrancy means two tasks access actor state simultaneously.”
No. Actor-isolated portions remain serialized. Reentrancy permits interleaving at suspension points, not simultaneous isolated execution.
“Every await definitely lets another actor job run.”
No. await marks a potential suspension point. Correct code must allow for interleaving without assuming that it occurs on every execution.
“An actor makes an async method transactional.”
No. Actor isolation protects each uninterrupted isolated segment. If an invariant crosses an await, the method must validate it after resuming.
“MainActor methods are exempt from reentrancy.”
No. A @MainActor async function can suspend, and other main-actor work may execute before it resumes. UI state can therefore face the same stale-result ordering problem.
13 · TUTORIAL
What to Remember
• An actor executes only one actor-isolated portion of code at a time.
• When an actor-isolated function suspends, another actor job may run.
• Actor state may therefore change across an await.
• Reentrancy is interleaving, not simultaneous access to isolated state.
• await marks possible suspension; it does not guarantee suspension.
• Actors prevent data races but do not make async methods atomic transactions.
• Capture operation identity before suspension and validate it after resuming.
• Keep related state mutations inside synchronous actor-isolated segments.
14 · TUTORIAL
Frequently Asked Questions
What does actor reentrancy mean in Swift?
It means another eligible job may execute on an actor while an earlier actor-isolated function is suspended. The earlier function can resume later.
Can actor state change during await?
Yes. If the current function suspends, another actor job can run and mutate isolated state before the original function resumes.
Does actor reentrancy cause data races?
Not on properly isolated actor state. The actor still serializes isolated access. Reentrancy can instead expose higher-level ordering mistakes.
Are synchronous actor methods reentrant?
A synchronous actor-isolated method has no suspension point, so another job cannot interleave in the middle of its execution on that actor.
Does await always release an actor?
No. await marks a possible suspension. If suspension occurs, the actor may run other eligible work; if the operation completes synchronously, no such gap is required.
How do I protect state across await?
Capture the relevant identity or version before awaiting, then re-read and validate actor state after resuming. Keep multi-property state changes in synchronous actor-isolated code.
15 · TUTORIAL
References
16 · TUTORIAL
Continue Learning
Reentrancy explains how data-race-free actor operations can still complete in an unwanted order. The next article, What Is a Logical Race in Swift Concurrency?, examines stale responses, competing feature operations and the difference between memory safety and correct application behaviour.
17 · TUTORIAL
Download the Xcode Playgrounds
Use Understanding Actor Reentrancy.playground to step through controlled actor interleaving, then open Actor Reentrancy Challenges.playground to practise identifying assumptions that cross await and protecting state without relying on timing.
