top of page

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:

  • @MainActor for main-domain isolation;
  • actors for isolated mutable state;
  • structured tasks for concurrent child operations;
  • and ordinary async/await control 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.

bottom of page