top of page

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.

bottom of page