01 · INTRODUCTION
What Is an Actor in Swift?
The short answer
An actor is a Swift reference type that protects its mutable state from being accessed by multiple tasks at the same time.
CODE EXAMPLE
actor LaunchStore {
private var launches: [Launch] = []
func save(_ launch: Launch) {
launches.append(launch)
}
}
The actor owns launches. Code outside the actor cannot freely read or change that array. It must ask the actor to perform an operation through one of its isolated methods.
The central idea: an actor turns mutable state into an isolated resource with one controlled entrance.
02 · TUTORIAL
The Problem an Actor Solves
A normal class can be shared between concurrent tasks.
CODE EXAMPLE
final class LaunchStore {
var launches: [Launch] = []
}
let store = LaunchStore()
Task {
store.launches.append(spacexLaunch)
}
Task {
store.launches.append(nasaLaunch)
}
Both tasks can reach the same mutable array. If their operations overlap, the program has a data race. The result is undefined behaviour: data can be lost, corrupted or cause the process to fail.
The problem is not that an array exists. The problem is that shared mutable state has no enforced owner.
03 · TUTORIAL
An Actor Owns Its Mutable State
Changing class to actor creates an isolation boundary around the stored properties and instance methods.
CODE EXAMPLE
actor LaunchStore {
private var launches: [Launch] = []
func save(_ launch: Launch) {
launches.append(launch)
}
func allLaunches() -> [Launch] {
launches
}
}
Inside LaunchStore, its methods can access launches directly because they are isolated to the same actor instance.
Outside the actor, the property is no longer ordinary shared state. Callers cross the actor boundary through methods such as save(_:) and allLaunches().
Actor-isolated state is state whose access is protected by a particular actor instance.
04 · TUTORIAL
Each Actor Instance Has Its Own Isolation Domain
MainActor provides one global isolation domain. A normal actor creates a separate isolation domain for every instance.
CODE EXAMPLE
let upcomingLaunches = LaunchStore()
let previousLaunches = LaunchStore()
These two stores do not share one actor boundary. Each instance owns its own state and coordinates its own isolated operations.
The following is a conceptual diagram:
EXECUTION DIAGRAM
upcomingLaunches ─▶ Actor instance A ─▶ isolated launches array A
previousLaunches ─▶ Actor instance B ─▶ isolated launches array B
A job executing on actor A does not automatically prevent a job from executing on actor B. The isolation rule belongs to each actor instance.
05 · TUTORIAL
Calling an Actor from Outside Requires await
A task outside an actor cannot assume that the actor is immediately available.
CODE EXAMPLE
let store = LaunchStore()
await store.save(spacexLaunch)
let launches = await store.allLaunches()
The await marks a potential suspension point. If another eligible job is using the actor, the current task can suspend until its call can run.
The actor methods themselves do not need to be declared async:
CODE EXAMPLE
func save(_ launch: Launch) {
launches.append(launch)
}
save(_:) contains no asynchronous operation. The call is asynchronous only when a caller must cross into the actor’s isolation domain.
await can describe access to an isolated resource, not only waiting for a network request or timer.
06 · TUTORIAL
Calls from Inside the Actor Are Direct
Actor-isolated methods already have permission to access the actor’s state and call its other isolated methods.
CODE EXAMPLE
actor LaunchStore {
private var launches: [Launch] = []
func save(_ launch: Launch) {
launches.append(launch)
sortByLaunchDate()
}
private func sortByLaunchDate() {
launches.sort { $0.date < $1.date }
}
}
save(_:) does not use await to call sortByLaunchDate(). Both methods belong to the same actor instance, so no isolation boundary is crossed.
07 · TUTORIAL
An Actor Processes Isolated Work Serially
When several tasks call the same actor, their calls become jobs that require that actor’s executor.
CODE EXAMPLE
async let first: Void = store.save(spacexLaunch)
async let second: Void = store.save(nasaLaunch)
async let third: Void = store.save(esaLaunch)
_ = await (first, second, third)
The child tasks are concurrent, but the isolated bodies of the three save(_:) calls do not execute on that actor at the same time.
The following is a conceptual execution diagram:
EXECUTION DIAGRAM
Task A ─┐
Task B ─┼─▶ LaunchStore actor ─▶ save A ─▶ save C ─▶ save B
Task C ─┘
The exact order should not be assumed. Actor isolation guarantees mutually exclusive execution of actor-isolated code, not first-in-first-out processing.
08 · TUTORIAL
An Actor Is Not a Thread
An actor does not create or permanently own a system thread.
The layers remain the same ones established earlier in this series:
EXECUTION DIAGRAM
Task
│ produces an eligible job
▼
Actor's serial executor
│ schedules execution
▼
System thread
│ runs instructions
▼
Processor core
Different jobs for the same actor may execute on different system threads at different times. The actor’s guarantee is serial isolation, not thread affinity.
This is an important shift in how we describe the code. Instead of choosing and protecting a thread, we define which actor owns the state. Swift Concurrency coordinates the execution needed to preserve that ownership rule.
09 · TUTORIAL
An Actor Is Not a Queue of Closures
An actor can initially resemble a private serial dispatch queue, but it expresses more information.
CODE EXAMPLE
actor LaunchStore {
private var launches: [Launch] = []
func save(_ launch: Launch) {
launches.append(launch)
}
}
The compiler understands that launches belongs to LaunchStore. It can diagnose an invalid access at the call site. With a manually managed queue, the compiler does not know that every access to a property must pass through that queue.
An actor therefore combines three ideas:
• A reference to an independently owned piece of state.
• A language-enforced isolation boundary.
• A serial executor for actor-isolated jobs.
10 · TUTORIAL
Suspension Does Not Lock the Actor
An actor method can itself be asynchronous.
CODE EXAMPLE
actor LaunchStore {
private var launches: [Launch] = []
func refresh(using api: LaunchAPI) async throws {
let downloaded = try await api.fetchUpcomingLaunches()
launches = downloaded
}
}
When refresh(using:) reaches await, its task may suspend. The task does not hold a lock or reserve the actor while the request is in progress.
Another eligible job can enter LaunchStore during that suspension.
EXECUTION DIAGRAM
refresh job: run ─────── suspend ───────────── resume
actor: refresh ── save another launch ─ apply download
This cooperative behaviour keeps the actor available. It also means that actor state may change between the code before an await and the code after it.
11 · TUTORIAL
Actor Methods Are Reentrant
Consider a store that replaces launches for a selected agency:
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 selectedAgency == requestedAgency else {
return
}
launches = result
}
}
While the request is suspended, another actor call may change selectedAgency. The method checks its assumption after resuming before applying the result.
This is actor reentrancy. It is not simultaneous execution inside the actor. It is separate actor-isolated jobs making progress while an earlier task is suspended. The series will examine it fully in a dedicated article.
12 · TUTORIAL
Return Values Instead of Exposing Mutable State
An actor should provide operations that preserve its own rules.
CODE EXAMPLE
actor LaunchStore {
private var launches: [Launch] = []
func save(_ launch: Launch) {
guard !launches.contains(where: { $0.id == launch.id }) else {
return
}
launches.append(launch)
launches.sort { $0.date < $1.date }
}
func snapshot() -> [Launch] {
launches
}
}
The caller asks the actor for a snapshot rather than receiving unrestricted access to the actor’s stored array. The duplicate check, insertion and sorting all happen inside one synchronous period of actor-isolated execution.
This is stronger than treating the actor as a container with getters and setters. The actor’s methods can describe valid changes to the state it owns.
13 · TUTORIAL
Actors and MainActor Have Different Roles
A feature can use a normal actor for independently shared model state and MainActor for visible application state.
CODE EXAMPLE
actor LaunchStore {
private var launches: [Launch] = []
func replace(with launches: [Launch]) {
self.launches = launches
}
func snapshot() -> [Launch] {
launches
}
}
@MainActor
final class LaunchListFeature: ObservableObject {
@Published private(set) var launches: [Launch] = []
private let store: LaunchStore
init(store: LaunchStore) {
self.store = store
}
func reloadFromStore() async {
launches = await store.snapshot()
}
}
LaunchStore and LaunchListFeature are separate isolation domains. The feature crosses into the store with await, receives a value and then updates its own main-actor-isolated state.
This gives the architecture explicit ownership:
• LaunchStore owns its model state.
• LaunchListFeature owns the state presented to the user.
• Tasks move values and requests between those boundaries.
14 · TUTORIAL
The Complete Mental Model
When a task calls a method on an actor, use this model:
EXECUTION DIAGRAM
Task calls actor method
│
▼
Does the task already have access to this actor's isolation?
│
├─ yes ─▶ call directly
│
└─ no ─▶ await the actor boundary
│
▼
actor-isolated job
│
▼
actor's serial executor
│
▼
method accesses actor state
Only one actor-isolated job executes on that actor at a time. If the running task suspends, another eligible job may execute before the original task resumes.
The actor is not the task, executor, thread or processor. It is the owner and isolation boundary for its state.
15 · TUTORIAL
What to Remember
• An actor is a reference type that protects its mutable state.
• Every actor instance has its own isolation domain.
• Code outside the actor usually crosses into it with await.
• A synchronous actor method can require an asynchronous call from outside.
• Actor-isolated code executes serially, but submission order is not guaranteed.
• An actor is not a thread and does not permanently own one.
• A suspended task does not lock or reserve the actor.
• Other actor jobs may run across an await, so earlier assumptions may need checking.
• Actor methods should express valid operations instead of exposing mutable state.
16 · TUTORIAL
Frequently Asked Questions
Is an actor just a thread-safe class?
An actor is a reference type with language-enforced isolation. “Thread-safe class” describes a desired result; an actor gives the compiler rules for how its state may be accessed.
Does every actor have its own thread?
No. An actor has a serial executor, not a permanently assigned thread. Actor jobs can be run by system threads as the runtime schedules them.
Why do I need await for a method that is not async?
Because the caller is crossing an actor boundary and may need to suspend until the actor can run the call. The method body does not need to contain its own suspension point.
Can two methods execute on the same actor at once?
Two actor-isolated method bodies do not execute simultaneously on the same actor. If one suspends, however, another job can execute before the first resumes.
Does an actor guarantee that calls run in order?
No. It guarantees serial execution of actor-isolated jobs, not strict first-in-first-out ordering between concurrent callers.
Should every class become an actor?
No. Use an actor when independently shared mutable state needs an isolation boundary. Immutable values, UI-facing types already owned by MainActor and types confined to one context may need different designs.
17 · TUTORIAL
Continue Learning
An actor protects state because Swift restricts which code may access that state directly. The next article, What Is Actor Isolation in Swift?, will examine those compiler rules precisely: what becomes isolated, which accesses require await, what nonisolated means and how isolation can be inherited by a function or closure.
18 · TUTORIAL
Download the Xcode Playground
Use the accompanying playground to replace a shared LaunchStore class with an actor. Start several child tasks that save launches, observe where await becomes necessary and add a suspension inside an actor method to see another actor job make progress before the first resumes.
