01 · INTRODUCTION
What Is Actor Isolation in Swift?
The short answer
Actor isolation is Swift’s rule that actor-owned mutable state can only be accessed by code running within that actor’s isolation domain.
CODE EXAMPLE
actor LaunchStore {
private var launches: [Launch] = []
func save(_ launch: Launch) {
launches.append(launch)
}
}
The array, the save(_:) method and the code executing inside that method are isolated to this particular LaunchStore instance. Code outside the actor must cross the isolation boundary before it can ask the actor to use that state.
The central idea: isolation defines which code has permission to access a piece of mutable state directly.
02 · TUTORIAL
Isolation Is the Rule That Makes Actors Safe
An actor does not protect state merely because its declaration uses a different keyword. It protects state because the compiler applies isolation rules to its members.
CODE EXAMPLE
actor LaunchStore {
var launches: [Launch] = []
}
let store = LaunchStore()
// Error: actor-isolated property cannot be mutated from here.
store.launches.append(spacexLaunch)
The compiler rejects this access before the program runs. The caller is outside store’s isolation domain and therefore has no permission to mutate its array directly.
This changes concurrency safety from a convention into a language rule. A comment saying “only use this property on our serial queue” can be forgotten. Actor isolation is checked at every access the compiler can see.
03 · TUTORIAL
Each Actor Instance Defines a Boundary
Isolation belongs to an actor instance, not simply to the actor type.
CODE EXAMPLE
let upcomingStore = LaunchStore()
let historicalStore = LaunchStore()
upcomingStore and historicalStore are separate isolation domains. Code isolated to one instance does not automatically gain direct access to the other instance.
The following is a conceptual diagram:
EXECUTION DIAGRAM
Task A ─▶ upcomingStore isolation ─▶ upcoming launches
Task B ─▶ historicalStore isolation ─▶ historical launches
The two actor instances can make progress independently. The serial execution rule applies within each boundary, not across every instance of LaunchStore.
04 · TUTORIAL
Stored State Is Actor-Isolated by Default
An actor’s mutable stored properties belong to that actor instance.
CODE EXAMPLE
actor LaunchStore {
private var launches: [Launch] = []
private var lastUpdated: Date?
func save(_ launch: Launch) {
launches.append(launch)
lastUpdated = Date()
}
}
Inside save(_:), both properties are directly available. The method is isolated to the same actor, so the compiler knows that this code has entered the correct boundary.
Outside the actor, even a read may require isolated access:
CODE EXAMPLE
actor LaunchStore {
var count: Int {
launches.count
}
}
let numberOfLaunches = await store.count
A read is still code executing against actor-owned state. It must be coordinated with possible writes.
05 · TUTORIAL
Actor Methods Inherit the Actor’s Isolation
Instance methods are actor-isolated unless they are explicitly declared otherwise.
CODE EXAMPLE
actor LaunchStore {
private var launches: [Launch] = []
func contains(id: Launch.ID) -> Bool {
launches.contains { $0.id == id }
}
func saveIfNeeded(_ launch: Launch) {
guard !contains(id: launch.id) else {
return
}
launches.append(launch)
}
}
saveIfNeeded(_:) can call contains(id:) synchronously. Both methods are isolated to the same actor instance, so the call remains inside one boundary.
From outside the actor, the same method requires a crossing:
CODE EXAMPLE
await store.saveIfNeeded(spacexLaunch)
06 · TUTORIAL
await Marks a Possible Isolation Crossing
The method saveIfNeeded(_:) is not declared async. Its body has no suspension point. The external call still requires await because the task may need to wait for permission to enter the actor.
EXECUTION DIAGRAM
Outside task
│
│ await store.saveIfNeeded(...)
▼
LaunchStore isolation boundary
│
▼
saveIfNeeded executes against actor-owned state
If the actor is available, the call may proceed without an observable delay. If another eligible job is executing there, the caller can suspend. As always, await means suspension is possible, not guaranteed.
Isolation crossing is a call or access that moves from one isolation domain into another.
07 · TUTORIAL
Isolation Is About Permission, Not the Current Thread
Actor isolation does not mean “this property belongs to thread 4.” It means “this property belongs to this actor.”
EXECUTION DIAGRAM
Actor-isolated function
│ creates eligible work
▼
Actor's serial executor
│ schedules it
▼
Available system thread
│ executes it
▼
Processor core
An actor-isolated job can execute on different system threads at different times. Direct access remains safe because Swift preserves the actor boundary, not because every instruction remains attached to one thread.
This is why thread checks cannot replace isolation checks. The actor is the owner. A thread is an execution resource.
08 · TUTORIAL
Isolation Does Not Mean One Task Owns the Actor
An asynchronous actor method can suspend while waiting for another operation.
CODE EXAMPLE
actor LaunchStore {
private var launches: [Launch] = []
func refresh(using api: LaunchAPI) async throws {
let downloaded = try await api.fetchUpcomingLaunches()
launches = downloaded
}
}
Before the await, the task executes within LaunchStore isolation. While suspended, it does not reserve the actor. Another eligible actor-isolated job can execute.
EXECUTION DIAGRAM
refresh task: actor code ── suspended ───────── actor code
other task: actor code
Both periods of refresh(using:) are isolated, but the entire asynchronous method is not one uninterrupted transaction.
09 · TUTORIAL
Isolation Prevents Data Races, Not Logical Mistakes
Because another job can run during a suspension, earlier assumptions may no longer be true when the task resumes.
CODE EXAMPLE
actor LaunchStore {
private var selectedAgency: Agency
private var launches: [Launch] = []
func refresh(using api: LaunchAPI) async throws {
let requestedAgency = selectedAgency
let result = try await api.fetchLaunches(for: requestedAgency)
guard requestedAgency == selectedAgency else {
return
}
launches = result
}
}
The actor prevents two jobs from reading and writing its storage simultaneously. It cannot decide whether an older network response is still meaningful. That is an application rule, so the method verifies it after suspension.
This is the beginning of actor reentrancy. For this article, the important distinction is simple: isolation prevents simultaneous unsafe access; it does not freeze actor state across await.
10 · TUTORIAL
MainActor Uses the Same Isolation Model
MainActor is a global actor, but the compiler applies the same core rule: isolated state can be accessed directly only from code in the same isolation domain.
CODE EXAMPLE
@MainActor
final class LaunchListFeature: ObservableObject {
@Published private(set) var launches: [Launch] = []
func display(_ launches: [Launch]) {
self.launches = launches
}
}
A main-actor-isolated function can call display(_:) directly. Code outside MainActor crosses the boundary with await.
CODE EXAMPLE
func apply(
_ launches: [Launch],
to feature: LaunchListFeature
) async {
await feature.display(launches)
}
The difference is scope. Each normal actor instance creates its own domain. MainActor provides one globally shared domain for main-facing state and operations.
11 · TUTORIAL
A Task Can Inherit Isolation
A task created while code is actor-isolated can inherit that actor context.
CODE EXAMPLE
actor LaunchStore {
private var launches: [Launch] = []
func scheduleSort() {
Task {
launches.sort { $0.date < $1.date }
}
}
}
The task closure can access launches because it inherits LaunchStore isolation from scheduleSort(). Creating the task does not discard the boundary or automatically move the operation to a background thread.
This inheritance is useful because asynchronous work can remain connected to the state it is meant to update. It is also why creating Task { } is not an escape hatch from actor isolation.
12 · TUTORIAL
What nonisolated Means
Sometimes an actor member does not need access to isolated state. It can be declared nonisolated.
CODE EXAMPLE
actor LaunchStore {
nonisolated let sourceName: String
private var launches: [Launch] = []
init(sourceName: String) {
self.sourceName = sourceName
}
nonisolated func makeLogPrefix() -> String {
"[\(sourceName)]"
}
}
A caller can use the nonisolated function without crossing into the actor:
CODE EXAMPLE
let prefix = store.makeLogPrefix()
Inside makeLogPrefix(), the compiler does not grant access to isolated mutable state.
CODE EXAMPLE
nonisolated func currentCount() -> Int {
// Error: launches is actor-isolated.
launches.count
}
nonisolated removes an actor-isolation requirement. It does not mean “execute in the background.”
13 · TUTORIAL
Using an isolated Parameter
A function outside an actor can explicitly borrow an actor’s isolation through an isolated parameter.
CODE EXAMPLE
actor LaunchStore {
private var launches: [Launch] = []
func count() -> Int {
launches.count
}
func nextLaunch() -> Launch? {
launches.min { $0.date < $1.date }
}
}
func makeSummary(
for store: isolated LaunchStore
) -> String {
let count = store.count()
let next = store.nextLaunch()
return "\(count) launches. Next: \(next?.name ?? "None")"
}
Inside makeSummary(for:), calls on store are synchronous because the function itself runs in that actor instance’s isolation.
The caller crosses the boundary once:
CODE EXAMPLE
let summary = await makeSummary(for: store)
An isolated parameter is useful when one operation needs to perform several synchronous actions within one actor boundary. A function can borrow one actor’s isolation at a time.
14 · TUTORIAL
Values Leaving an Actor Raise a New Question
An actor often returns a value to a task outside its isolation domain.
CODE EXAMPLE
actor LaunchStore {
private var launches: [Launch] = []
func snapshot() -> [Launch] {
launches
}
}
let launches = await store.snapshot()
The method access is isolated, but the returned value leaves the actor. Swift therefore needs to know whether that value can safely cross between concurrency domains.
That question is not answered by actor isolation alone. It leads directly to Sendable, which describes values that can safely be transferred across these boundaries.
15 · TUTORIAL
The Complete Mental Model
For every access to actor-owned state, ask one question: where is the current code isolated?
EXECUTION DIAGRAM
Code accesses actor member
│
▼
Same actor isolation?
│
├─ yes ─▶ direct access is permitted
│
└─ no ─▶ cross boundary with await
│
▼
actor's serial executor
│
▼
isolated access executes
nonisolated members sit outside the actor boundary and cannot freely access isolated state. An isolated parameter places a whole function inside the boundary of the supplied actor.
If isolated code suspends, another actor job may run. The state remains protected from simultaneous access, but it may change before the original task resumes.
16 · TUTORIAL
What to Remember
• Actor isolation defines which code may directly access actor-owned mutable state.
• The compiler enforces the boundary at each access.
• Isolation belongs to an actor instance, not merely to its type.
• Actor properties and instance methods are isolated by default.
• Calls inside the same isolation domain are direct.
• Calls from outside generally cross the boundary with await.
• Isolation is an ownership rule, not thread affinity.
• Suspension allows another isolated job to run, so state can change across await.
• nonisolated removes isolation; it does not request background execution.
• An isolated parameter lets a function borrow one actor instance’s isolation.
17 · TUTORIAL
Frequently Asked Questions
What is the difference between an actor and actor isolation?
An actor is the reference type and owner of state. Actor isolation is the compiler-enforced rule controlling access to that state.
Does actor isolation mean the actor uses one thread?
No. Isolation restricts access through an actor’s serial executor. The system threads that execute actor jobs can change over time.
Why does reading an actor property require await?
A read must be coordinated with possible writes to the same state. From outside the actor, the task may need to suspend before that isolated access can execute.
Does await make the rest of a function actor-isolated?
No. await marks a potential suspension and permits a particular asynchronous call. It does not permanently move all surrounding code into the called actor.
Does nonisolated make a function concurrent?
No. It states that the function does not require the actor’s isolation. It says nothing by itself about parallel execution or background threads.
Can actor state change during an isolated method?
Not simultaneously while one synchronous section is executing. If the method suspends at await, another actor-isolated job may run and change the state before the method resumes.
18 · TUTORIAL
Continue Learning
Actor isolation controls access to state, but values still need to move between actors, tasks and global actors. The next article, What Is Sendable in Swift?, will explain how Swift describes values that are safe to transfer across concurrency boundaries.
19 · TUTORIAL
Download the Xcode Playground
Use the accompanying playground to inspect which LaunchStore accesses compile inside and outside the actor. Add a nonisolated logging method, write a helper with an isolated parameter and then return a launch snapshot across the boundary to prepare for the next lesson on Sendable.
