What Is an Executor in Swift?
An executor is a service that accepts eligible Swift concurrency jobs and arranges for system threads to execute them.
Tasks describe asynchronous operations. Jobs are their runnable portions. Executors connect those jobs to the threads on which their machine instructions can run.
TASK
│
▼
Eligible job
│
▼
EXECUTOR
│
▼
System thread
│
▼
Processor core
An executor is not a thread, and it is not the task itself.
💡 Important Terminology
Task = the complete lifetime of an asynchronous operation.
Job = an eligible synchronous portion of task work.
Executor = a service that accepts jobs and arranges their execution.
Thread = an operating-system execution context that runs machine instructions.
Why Does Swift Need Executors?
A task can suspend and later become ready to continue.
Task {
status = "Loading"
let launches = try await api.loadLaunches()
status = "Finished"
}
The task may contain two runnable jobs separated by suspension:
ONE TASK
JOB 1
status = "Loading"
begin loadLaunches()
│
▼
SUSPEND
│
▼
JOB 2
receive launches
status = "Finished"
When Job 2 becomes eligible, something must arrange for a thread to execute it.
That is the executor's role.
An Executor Accepts Jobs
At its simplest, an executor provides a destination for eligible work.
ELIGIBLE JOBS
[Job A] [Job B] [Job C]
│ │ │
└───────┴───────┘
│
▼
EXECUTOR
│
▼
Arrange for threads to run the jobs
The word arrange matters.
An executor does not need to own one permanently assigned thread. Its execution strategy can use system resources underneath according to its implementation and contract.
Most Swift code uses executors implicitly. The compiler and runtime determine which executor is appropriate from the current isolation and execution context.
An Executor Is Not a Thread
A thread is the resource that actually carries machine instructions to a processor core.
An executor is the scheduling service that arranges for a job to use an execution resource.
EXECUTOR
Accepts and arranges jobs
THREAD
Runs the selected job's instructions
PROCESSOR CORE
Executes those machine instructions
One executor can arrange different jobs on different threads over time.
One system thread can execute jobs associated with different execution contexts over its lifetime.
Therefore, “which executor?” and “which thread?” are different questions.
An Executor Is Not a Task
A task owns the logical operation.
An executor provides execution opportunities for its runnable jobs.
TASK: REFRESH LAUNCHES
Created
Running job 1
Suspended
Running job 2
Completed
EXECUTOR
Accepts job 1
Arranges its execution
Accepts job 2 when it becomes eligible
Arranges its execution
The task persists while suspended.
The executor has no runnable job from that task until its continuation becomes eligible.
General Executors and Serial Executors
Not every executor promises the same execution relationship.
EXECUTOR
Accepts jobs
May arrange jobs serially or concurrently
SERIAL EXECUTOR
Accepts jobs
Guarantees that its jobs do not execute concurrently
A serial executor totally orders its job executions: one job finishes before another job on that executor runs.
This does not imply first-in, first-out order.
SUBMITTED
Job A → Job B → Job C
ONE POSSIBLE SERIAL EXECUTION
Job B → Job A → Job C
No jobs overlap, but submission order is not promised.
An implementation may consider factors such as task priority when choosing its next job.
💡 Important
Serial means non-overlapping execution.
Serial does not automatically mean first-in, first-out.
Actors Use Serial Executors
An actor protects its isolated mutable state by allowing only one isolated job to execute at a time.
actor LaunchStore {
private var launches: [Launch] = []
func replace(with newLaunches: [Launch]) {
launches = newLaunches
}
}
The actor's serial executor helps uphold this mutual exclusion.
LAUNCH STORE ACTOR
│
▼
Serial executor
│
├── Replace job A
├── Read job B
└── Replace job C
Only one isolated job executes at a time.
The actor is the isolation domain.
The serial executor is the execution service used to arrange actor-isolated jobs without overlap.
An actor is not an executor, even though the two concepts work closely together.
The Main Actor Has an Executor
MainActor is a global actor representing the main isolation domain.
@MainActor
final class LaunchListFeature: ObservableObject {
@Published private(set) var launches: [Launch] = []
}
Jobs that access this isolated state must execute through the main actor's executor.
MAIN-ACTOR-ISOLATED JOBS
[Change loading state]
[Handle button action]
[Publish launches]
│
▼
MAIN ACTOR'S SERIAL EXECUTOR
│
▼
MAIN EXECUTION DOMAIN
On Apple platforms, main-actor execution is associated with the main thread.
The useful contract in Swift code is actor isolation: mark code @MainActor when it belongs to that isolation domain rather than manually checking or selecting a thread.
MainActor and the Main Thread Are Related but Different
The main thread is an operating-system execution context.
MainActor is Swift's global actor for isolated main-domain work.
The main actor's executor arranges its jobs on the main execution system.
MAIN ACTOR
Isolation domain
│
▼
MAIN ACTOR'S EXECUTOR
Arranges isolated jobs
│
▼
MAIN THREAD
Runs their machine instructions
These layers cooperate, but they are not interchangeable names for one object.
The distinction lets Swift describe safe access to UI state without requiring application code to organise every operation around raw threads.
What Happens When a Task Crosses to an Actor?
A task can call an actor-isolated function from outside that actor:
let launches = try await api.loadLaunches()
await store.replace(with: launches)
The call to store.replace belongs to the store actor's executor.
CURRENT TASK
│
▼
Reach actor-isolated call
│
▼
Store job becomes eligible
│
▼
STORE ACTOR'S SERIAL EXECUTOR
│
▼
Executor arranges the job
│
▼
replace(with:) executes in isolation
If execution must change to the actor's executor, the task may suspend while the actor-isolated job waits to run.
The task remains the same logical operation. What changes is the executor responsible for its current job.
A Task Can Use More Than One Executor
A task is not permanently attached to one executor for its complete lifetime.
func refresh() async throws {
let launches = try await api.loadLaunches()
await store.replace(with: launches)
await MainActor.run {
status = "Finished"
}
}
ONE TASK
API-related job
│
▼
possible suspension
│
▼
LaunchStore executor job
│
▼
possible suspension
│
▼
MainActor executor job
The task's logical lifetime crosses these execution contexts while preserving its control flow.
await makes potential suspension boundaries visible when asynchronous calls may require another execution context.
Executors Do Not Guarantee One Exact Thread
Except where an execution contract is specifically tied to a platform thread, Swift code should not assume that one executor always means one dedicated thread.
EXECUTOR JOB 1
│
▼
System thread A
EXECUTOR JOB 2
│
▼
System thread B
The runtime is intentionally flexible about thread use.
This allows it to use limited system execution resources without creating one permanent thread for every task, actor or executor.
Application correctness should normally depend on isolation guarantees rather than observed thread identities.
An Executor Is Not Necessarily a Dispatch Queue
Dispatch queues and executors both accept work, so the concepts can look similar.
They are not synonymous.
DISPATCH QUEUE
Grand Central Dispatch abstraction
Stores submitted blocks and targets execution resources
SWIFT EXECUTOR
Swift Concurrency abstraction
Accepts executor jobs and arranges their execution
A platform's default executor implementation may use Dispatch mechanisms underneath.
That implementation detail does not turn every executor into a DispatchQueue, nor should application code replace actor isolation reasoning with queue identity reasoning.
Executors Do Not Promise FIFO Scheduling
Creating tasks in a particular order does not establish a general guarantee that their jobs will execute in that order.
Task { @MainActor in
print("A")
}
Task { @MainActor in
print("B")
}
A particular run may print A followed by B.
That observation is not a replacement for an ordering relationship expressed by the program.
WRONG ASSUMPTION
Submitted first = guaranteed to execute first
SAFE MODEL
Serial executor prevents overlap
Program structure establishes required dependencies
Use control flow, task relationships and awaited dependencies when correctness requires one operation to follow another.
Why a Long Job Delays the Executor
A serial executor cannot run a second job concurrently with its current job.
@MainActor
func calculateLaunchWindows() {
for value in 1...500_000_000 {
performStep(value)
}
}
MAIN ACTOR'S SERIAL EXECUTOR
Calculation job
████████████████████████████████████████
UI state job
█████
Event-related job
█████
The executor does not make the loop concurrent with other main-actor work.
Cooperative scheduling requires the current synchronous job to finish or the task to reach a genuine suspension point before another job on the same serial executor can run.
The executor arranges execution. It does not make badly placed work cheap.
Most Developers Do Not Build Executors
Swift exposes protocols for custom executors, including serial executors used by actors.
These are low-level extension points intended for specialised requirements such as integrating with an existing event loop, queue or thread-bound system.
// Conceptual shape only — not ordinary app architecture.
final class SpecialExecutor: SerialExecutor {
func enqueue(_ job: consuming ExecutorJob) {
// Arrange for the job to run in a specialised system.
}
}
Most application code should not need to implement this machinery.
Developers normally express requirements with:
@MainActorfor main-domain isolation;- actors for isolated mutable state;
- structured tasks for concurrent child operations;
- and ordinary
async/awaitcontrol flow.
Swift then selects and uses the relevant executors.
💡 The Architectural Rule
Express where state is isolated.
Let Swift arrange the executor machinery unless the application has a genuine specialised integration requirement.
A Complete Feature Across Executors
An actor owns the stored launch data:
actor LaunchStore {
private var launches: [Launch] = []
func replace(with newLaunches: [Launch]) {
launches = newLaunches
}
func allLaunches() -> [Launch] {
launches
}
}
The main-actor feature owns visible state:
@MainActor
final class LaunchListFeature: ObservableObject {
@Published private(set) var launches: [Launch] = []
@Published private(set) var isLoading = false
private let api: LaunchAPI
private let store: LaunchStore
init(api: LaunchAPI, store: LaunchStore) {
self.api = api
self.store = store
}
func refresh() async {
isLoading = true
defer { isLoading = false }
do {
let loaded = try await api.loadLaunches()
await store.replace(with: loaded)
launches = await store.allLaunches()
} catch {
launches = []
}
}
}
The feature begins through the main actor's executor.
The task suspends while the network operation waits. It later enters the store actor's serial executor to replace and read the isolated data.
The task returns to the main actor's executor when it publishes the result to visible feature state.
ONE REFRESH TASK
MainActor executor
isLoading = true
│
▼
Await network operation
│
▼
LaunchStore serial executor
replace and read stored launches
│
▼
MainActor executor
publish launches and finish loading
The application code expresses isolation and control flow.
Executors provide the scheduling services that make those isolated jobs executable.
The Complete Mental Model
ASYNC FUNCTION EXECUTES IN A TASK
│
▼
TASK PRODUCES ELIGIBLE JOB
│
▼
APPROPRIATE EXECUTOR ACCEPTS JOB
│
├── general executor
│ may arrange concurrent execution
│
└── serial executor
prevents job overlap
│
▼
EXECUTOR ARRANGES A SYSTEM THREAD
│
▼
THREAD RUNS MACHINE INSTRUCTIONS
│
▼
JOB COMPLETES OR TASK SUSPENDS
An executor is the bridge between Swift's task jobs and the lower-level threads that execute them.
It accepts eligible jobs and arranges their execution according to its contract.
Serial executors prevent their jobs from overlapping, allowing actors to uphold isolated access to mutable state.
That is an executor in Swift.
What to Remember
💡 What to Remember
1. An executor accepts eligible jobs and arranges for threads to execute them.
2. A task is an asynchronous operation; an executor is an execution service.
3. An executor is not a thread.
4. General executors do not necessarily execute jobs serially.
5. A serial executor guarantees non-overlapping job execution.
6. Serial executors do not generally guarantee FIFO order.
7. Actors use serial executors to help uphold isolated access to state.
8. The main actor's executor arranges main-actor-isolated jobs on the main execution system.
9. A task can cross between executors during its lifetime.
10. Most developers select execution context through isolation rather than implementing custom executors.
Frequently Asked Questions
What is an executor in Swift?
An executor is a service that accepts eligible Swift concurrency jobs and arranges for system threads to run them.
Is an executor a thread?
No. A thread runs machine instructions. An executor schedules jobs onto execution resources and does not necessarily own one permanently assigned thread.
Is an executor a dispatch queue?
No. They are different abstractions. A platform executor may use Dispatch mechanisms internally, but Swift executors operate within Swift Concurrency's task, job and isolation model.
What is a serial executor?
A serial executor guarantees that its submitted jobs do not execute concurrently. The jobs are totally ordered, but their execution order is not necessarily their submission order.
Does every actor have its own thread?
No. An actor uses a serial executor to arrange isolated jobs. That executor can use system-managed threads underneath without dedicating one permanent thread to the actor.
Does every actor have its own executor?
Actors have an executor relationship used for their isolated work, but executor implementations may be shared. The important language contract is that actor-isolated jobs receive the required serial execution.
Do I need to create an executor?
Usually not. Most applications express execution requirements using actors, @MainActor and structured concurrency. Custom executors are advanced tools for specialised execution environments.
Continue Learning Swift Concurrency
Read What Is Cooperative Scheduling in Swift? to revisit how jobs return execution opportunities to executors.
Continue with What Is MainActor in Swift? to examine the global actor and serial execution domain used for important UI-facing state.
Swift Evolution's Structured Concurrency proposal defines the relationship between tasks, jobs and executors.
Download Xcode Playground
The accompanying What Is an Executor in Swift? Xcode playground can make execution contexts visible without depending on thread identity.
It should trace one task across a main-actor feature and a store actor, demonstrate that serial execution prevents job overlap and show that task submission order should not be used as a correctness guarantee.
A final experiment should run a long main-actor job while another main-actor task waits, connecting executor serialisation directly to visible UI delay.
The article explains the bridge. The playground will show each job crossing it.
