top of page

01 · INTRODUCTION

What Is @Sendable in Swift?

The short answer

@Sendable marks a function type whose value can safely cross concurrency boundaries. For a closure to satisfy that contract, the values it captures must also be safe to transfer.

CODE EXAMPLE

let formatLaunch: @Sendable (Launch) -> String = { launch in
    "\(launch.name) launches on \(launch.date)"
}

The closure can be passed to concurrent work because its function type is sendable. In this example, it captures no surrounding mutable state.

The central idea: Sendable checks the data crossing a boundary. @Sendable checks executable work and the data carried inside its captured environment.

02 · TUTORIAL

A Closure Is More Than Its Code

A closure contains a function body, but it can also capture values from the scope in which it was created.

CODE EXAMPLE

let agencyName = "NASA"

let describe = { (launch: Launch) in
    "\(agencyName): \(launch.name)"
}

The closure needs agencyName after the surrounding scope has been entered or even after it has returned. Swift stores that captured value as part of the closure’s context.

A useful conceptual model is:

EXECUTION DIAGRAM

Closure value
  ├─ function body
  └─ captured environment
       └─ agencyName

When a closure crosses into another task or actor, its captured environment travels with it. Swift must therefore check both the code’s function type and the safety of the values it carries.

03 · TUTORIAL

Why Ordinary Closures Are Not Automatically Sendable

An ordinary closure can capture a reference to mutable state.

CODE EXAMPLE

final class LaunchDraft {
    var name = "Untitled Launch"
}

let draft = LaunchDraft()

let rename = {
    draft.name = "Artemis II"
}

If this closure were freely transferred into concurrent domains, several tasks could invoke closures that reach the same unprotected LaunchDraft instance.

EXECUTION DIAGRAM

Task A ─▶ closure A ─┐
                     ├─▶ same mutable LaunchDraft
Task B ─▶ closure B ─┘

The danger is not visible in the closure’s parameter or return types. It is hidden in what the closure captured. This is the problem @Sendable makes visible to the compiler.

04 · TUTORIAL

@Sendable Is Part of the Function Type

The attribute appears on a function type before its parameter list.

CODE EXAMPLE

@Sendable (Launch) -> String

That type means: a function accepting a Launch, returning a String, and safe to transfer across concurrency boundaries.

CODE EXAMPLE

let formatter: @Sendable (Launch) -> String = { launch in
    launch.name.uppercased()
}

A sendable function value can be used where an ordinary function is expected. The reverse conversion is not necessarily safe because an ordinary closure may contain unchecked captures.

Sendable function type is a function type marked @Sendable whose values satisfy Swift’s rules for crossing concurrency domains.

05 · TUTORIAL

A Closure Can Receive @Sendable from Context

You do not always write the attribute inside the closure expression. An API can require a sendable function type:

CODE EXAMPLE

func processLaunches(
    _ operation: @Sendable (Launch) async -> Void
async {
    // The API may use operation in concurrent work.
}

The closure passed to that parameter is checked as @Sendable because the surrounding context requires it.

CODE EXAMPLE

await processLaunches { launch in
    print(launch.name)
}

This contextual inference is similar to the way a closure can be treated as @escaping because of the parameter receiving it. The call site can remain concise while the API preserves its concurrency contract.

06 · TUTORIAL

You Can Write @Sendable Explicitly

The attribute can also be written in the closure expression.

CODE EXAMPLE

let identify = { @Sendable (launch: Launch) in
    launch.id
}

This is useful when the closure is stored locally and its type would otherwise be inferred without sendability.

CODE EXAMPLE

let identify: @Sendable (Launch) -> Launch.ID = { launch in
    launch.id
}

Both forms communicate the same requirement. The first annotates the closure expression; the second states it in the variable’s function type.

07 · TUTORIAL

Capturing Sendable Constants Is Safe

A sendable closure may capture immutable values whose types conform to Sendable.

CODE EXAMPLE

let selectedAgency = "NASA"

let belongsToAgency: @Sendable (Launch) -> Bool = { launch in
    launch.agencyName == selectedAgency
}

String is sendable, and selectedAgency is an immutable value captured by value. The closure carries a safe snapshot of the information it needs.

EXECUTION DIAGRAM

Sendable closure
  ├─ comparison code
  └─ captured String value: "NASA"

The compiler builds closure safety from the sendability of every captured value.

08 · TUTORIAL

Capturing Mutable Local State Is Restricted

A sendable closure cannot safely capture a local variable by reference and mutate it while concurrent work may also access it.

CODE EXAMPLE

var processedCount = 0

let recordLaunch: @Sendable (Launch) -> Void = { launch in
    processedCount += 1
    // Error: mutation of captured var in concurrently executing code.
}

The variable’s type, Int, is sendable. The problem is not the integer type. The problem is that the closure captures mutable storage that also remains available to the surrounding scope.

Two executions could attempt to update the same captured variable:

EXECUTION DIAGRAM

Closure execution A ─┐
                     ├─▶ captured processedCount
Closure execution B ─┘

@Sendable prevents this unsafe shared mutation at compile time.

09 · TUTORIAL

A Capture List Can Take a Value Snapshot

If a closure only needs the current value of a mutable local variable, a capture list can capture that value explicitly.

CODE EXAMPLE

var selectedAgency = "NASA"

let filter: @Sendable (Launch) -> Bool = { [selectedAgency] launch in
    launch.agencyName == selectedAgency
}

selectedAgency = "ESA"

The closure keeps the value "NASA". Changing the outer variable later does not change the captured snapshot.

This repairs shared-storage capture only when snapshot semantics are correct for the feature. A capture list should express the intended behaviour, not merely silence a diagnostic.

10 · TUTORIAL

Capturing a Non-Sendable Reference Is Unsafe

A constant reference can still point to mutable state. Declaring the reference with let does not make its instance sendable.

CODE EXAMPLE

final class LaunchDraft {
    var name = "Untitled"
}

let draft = LaunchDraft()

let printDraft: @Sendable () -> Void = {
    print(draft.name)
    // Error: capture of non-Sendable type LaunchDraft.
}

The closure captures the class reference by value, but that copied reference still reaches the same mutable object. By-value capture and value semantics are not the same thing.

A captured let is only safe when the captured value’s type satisfies the sendability contract.

11 · TUTORIAL

Capturing an Actor Is Safe

An actor reference is sendable because callers cannot use it to bypass actor isolation.

CODE EXAMPLE

actor LaunchStore {
    private var launches: [Launch] = []

    func save(_ launch: Launch) {
        launches.append(launch)
    }
}

let store = LaunchStore()

let saveLaunch: @Sendable (Launch) async -> Void = { launch in
    await store.save(launch)
}

The closure may carry the store reference into another concurrency domain. Access to the array remains protected, and the closure must still cross the actor boundary with await.

@Sendable and actor isolation work together. Sendability permits the reference to travel; isolation controls how it can be used.

12 · TUTORIAL

@Sendable Does Not Mean @MainActor

A sendable closure has no main-actor isolation merely because it is safe to transfer.

CODE EXAMPLE

let format: @Sendable (Launch) -> String = { launch in
    launch.name
}

This closure is sendable, but it is not isolated to any particular actor.

A closure can carry both requirements when appropriate:

CODE EXAMPLE

let display: @MainActor @Sendable ([Launch]) -> Void = { launches in
    feature.display(launches)
}

@Sendable describes transfer safety. @MainActor states where the closure’s body must execute. They answer different questions.

13 · TUTORIAL

@Sendable Does Not Mean async

Sendability and suspension are independent parts of a function type.

CODE EXAMPLE

// Sendable and synchronous
@Sendable (Launch) -> String

// Sendable and asynchronous
@Sendable (Launch) async throws -> LaunchDetails

The first closure cannot suspend. The second can. Both are safe to transfer across concurrency boundaries.

Likewise, marking a closure @Sendable does not make it run concurrently, in parallel or on a background thread. It only makes the function value eligible to be used in contexts where such execution may occur.

14 · TUTORIAL

@Sendable and @escaping Answer Different Questions

An escaping closure can outlive the function call that received it. A sendable closure can cross concurrency boundaries safely.

CODE EXAMPLE

func registerProcessor(
    _ processor: @escaping @Sendable (Launch) -> Void
) {
    // Store processor for later use by concurrent work.
}

This parameter needs both guarantees:

@escaping permits the function to store the closure after returning.

@Sendable requires the stored closure and its captures to be safe across concurrency domains.

Neither attribute implies the other as a general conceptual rule. They describe different properties of the function value.

15 · TUTORIAL

Task Closures Carry Sendability Requirements

Task APIs use closure types designed for concurrent execution. The compiler therefore examines values captured by a task closure.

CODE EXAMPLE

let launch = Launch(
    id: UUID(),
    name: "Europa Clipper",
    date: launchDate
)

Task {
    await store.save(launch)
}

Launch and LaunchStore are sendable, so the closure can safely carry both values. The task does not gain direct access to the actor’s storage; it still calls save(_:) through actor isolation.

This article does not need to redefine Task. The important connection is that task creation is one common place where closure capture becomes part of concurrency safety.

16 · TUTORIAL

Design APIs That State Their Concurrency Contract

If an API may invoke a closure from concurrent work, its parameter type should express that requirement.

CODE EXAMPLE

struct LaunchProcessor {
    let process: @Sendable (Launch) async throws -> Void
}

func importLaunches(
    _ launches: [Launch],
    using process: @Sendable (Launch) async throws -> Void
async throws {
    for launch in launches {
        try await process(launch)
    }
}

The annotation does not promise that this implementation processes launches concurrently—the loop shown is sequential. It states that the supplied operation is safe for the API to transfer or use from a concurrent context.

This keeps concurrency safety at the API boundary instead of relying on documentation that callers may overlook.

17 · TUTORIAL

A Complete Rocket Launch Example

The earlier pieces can now be combined into one small feature:

CODE EXAMPLE

struct Launch: Sendable {
    let id: UUID
    let name: String
    let agencyName: String
}

actor LaunchStore {
    private var launches: [Launch] = []

    func save(_ launch: Launch) {
        launches.append(launch)
    }
}

func makeImporter(
    store: LaunchStore,
    agencyName: String
) -> @Sendable (Launch) async -> Void {
    { launch in
        guard launch.agencyName == agencyName else {
            return
        }

        await store.save(launch)
    }
}

The returned closure captures two values:

agencyName is an immutable String, which is sendable.

store is an actor reference, which is sendable and preserves its isolation.

The Launch parameter is sendable. The closure can therefore move into concurrent work without carrying unsafe shared mutable state.

EXECUTION DIAGRAM

Sendable closure
  ├─ captured String
  ├─ captured LaunchStore actor
  └─ receives Launch value
           │
           ▼
all carried values are Sendable


18 · TUTORIAL

The Complete Mental Model

When a closure is required to be @Sendable, inspect the environment it carries:

EXECUTION DIAGRAM

Closure crosses concurrency boundary
                 │
                 ▼
       What does it capture?
                 │
       ┌─────────┴─────────┐
       │                   │
Sendable values      Unsafe mutable state
       │                   │
       ▼                   ▼
Crossing allowed     Compiler diagnostic

Immutable sendable values, independent value types and actor references are common safe captures. Mutable local storage and non-sendable class references require a different ownership design.

The attribute changes the function value’s concurrency contract. It does not choose an executor, create a task, add actor isolation or request parallel execution.


19 · TUTORIAL

What to Remember

@Sendable marks a function type that is safe to transfer across concurrency domains.

• A closure value contains both code and a captured environment.

• Captured values in a sendable closure must satisfy sendability rules.

• Immutable sendable values can be captured safely.

• Mutable local variables cannot be captured as shared mutable storage.

• A capture list can take a value snapshot when snapshot semantics are correct.

• A let reference is not safe if its referenced type is non-sendable.

• Actors can be captured because their state remains isolated.

@Sendable does not mean async, @MainActor, background execution or parallelism.

• APIs should require @Sendable when they may transfer a closure into concurrent work.


20 · TUTORIAL

Frequently Asked Questions

What is the difference between Sendable and @Sendable?

Sendable is a protocol for types whose values can cross concurrency boundaries. @Sendable marks function types and causes Swift to check a closure’s captured environment.

Does @Sendable start a new task?

No. It only describes the function value’s safety. A separate API such as Task creates asynchronous work.

Does @Sendable run a closure on a background thread?

No. The attribute does not select an executor or thread. The context that invokes the closure determines where its work is eligible to execute.

Can a @Sendable closure capture a var?

It cannot capture mutable local storage in a way that concurrent executions could access unsafely. A capture list can take an immutable value snapshot when that matches the intended behaviour.

Can a @Sendable closure capture an actor?

Yes. Actor references are sendable. The closure must still use await when crossing into the actor’s isolated methods.

Are all Task closures @Sendable?

Task creation APIs impose sendability requirements on their operation closures, although isolation and task-region rules can affect how particular captures are checked. The practical rule is to treat task captures as part of your concurrency design.


22 · TUTORIAL

Continue Learning

We now understand tasks, suspension, executors, actors, isolation and the safe values and closures that move between them. The next article, What Is Structured Concurrency in Swift?, will explain how parent and child tasks form a bounded tree of work with lifetime, cancellation and result relationships.

23 · TUTORIAL

Download the Xcode Playground

Use the accompanying playground to build sendable launch filters and import operations. Capture a safe String, then attempt to capture mutable local state and a non-sendable class. Repair each design using a value snapshot or actor-owned state and pass the final closure into a task.

bottom of page