Sendable & Data-Race Safety

Reviewed & published by Brayan K

Swift 6 makes data-race safety a compile-time guarantee. Learn the Sendable protocol, @Sendable closures, actor isolation, @MainActor, sending values across isolation domains, nonisolated, and why the compiler now enforces all of this.

Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ The Sendable Protocol

Sendable marks a type as safe to share across concurrency domains. Value types built from Sendable members conform automatically, so most structs are already safe.

// Sendable marks a type SAFE to share across concurrency domains
// (between tasks or actors) without causing data races.

// Value types of Sendable members are AUTOMATICALLY Sendable.
struct User: Sendable {
    let id: Int
    let name: String
}

// Most simple value types already conform implicitly.
let u = User(id: 1, name: "Ada")

// You can pass a Sendable value into a Task safely:
Task {
    print("Sent across to a task: \(u.name)")  // Ada
}

2️⃣ @Sendable Closures

An @Sendable closure may only capture Sendable values, so it is safe to run concurrently. A Task's body is exactly such a closure.

// @Sendable closures may ONLY capture Sendable values, so they are
// safe to run concurrently. Task's body is an @Sendable closure.

func runConcurrently(_ work: @Sendable @escaping () -> Void) {
    Task { work() }
}

let greeting = "Hello"   // a Sendable String

runConcurrently {
    // Capturing 'greeting' is fine — String is Sendable.
    print(greeting)      // Hello
}

// Capturing a mutable, non-Sendable reference here would be a
// compiler error under strict concurrency.

3️⃣ Actor Isolation, @MainActor, nonisolated

An actor isolates mutable state, making it safely shareable. @MainActor pins code to the main thread; nonisolated opts a member out so it needs no await.

// Actors isolate mutable state; @MainActor pins code to the main actor.
actor BankAccount {
    private var balance = 0                     // isolated state

    func deposit(_ amount: Int) { balance += amount }
    func currentBalance() -> Int { balance }

    // nonisolated opts OUT of isolation — no await needed to call it.
    nonisolated let currency = "USD"
}

@MainActor
func updateLabel(_ text: String) {              // runs on the main actor
    print("UI: \(text)")
}

let account = BankAccount()
Task {
    await account.deposit(100)                  // cross-actor -> await
    let total = await account.currentBalance()
    print("Currency \(account.currency), total \(total)")  // no await on currency
}

Your turn. Fill in the safety protocol and the closure attribute.

// 🎯 YOUR TURN — fill in each ___ then run it in Xcode (Swift 6 mode).

// 1) Mark this value type as safe to share across domains.
struct Point: ___ {                    // 👉 the protocol for cross-domain safety
    let x: Int
    let y: Int
}

// 2) This closure must only capture Sendable values.
func schedule(_ job: ___ @escaping () -> Void) {   // 👉 the closure attribute
    Task { job() }
}

let p = Point(x: 1, y: 2)
schedule { print("Point \(p.x), \(p.y)") }   // Point 1, 2

// ✅ Expected output:
//    Point 1, 2

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

ConceptCodeMeaning
Safe to sharestruct S: Sendableno data races
Safe closure@Sendable () -> Voidcaptures Sendable
Isolated stateactor A { … }serialized access
Main thread@MainActorUI-safe
Opt outnonisolated let xno await
Manual promise@unchecked Sendableyou guarantee it

Mini-Challenge: Make It Race-Free

Wrap a shared counter in an actor so many tasks can update it safely.

// 🎯 MINI-CHALLENGE: make it race-free
// You have a counter shared by many tasks. Make it safe.
// 1. Define  actor Counter  with a private var value = 0.
// 2. Add  func increment()  and  func current() -> Int.
// 3. From a Task, await two increments, then await current() and print it.
//
// ✅ Expected output:
//    2

// your code here

Practice quiz

What does the Sendable protocol indicate about a type?

  • It is safe to share across concurrency domains
  • It is a class
  • It is faster
  • It can be serialized to JSON

Answer: It is safe to share across concurrency domains. Sendable marks a type as safe to pass across actor or task boundaries without data races.

Which kind of type is automatically Sendable when its members are Sendable?

  • A global variable
  • A class with mutable state
  • A value type like a struct of Sendable values
  • Any closure

Answer: A value type like a struct of Sendable values. Value types such as structs and enums made of Sendable members get Sendable conformance for free.

What does marking a closure @Sendable promise?

  • It never returns
  • It captures only values safe to use across concurrency domains
  • It is async
  • It runs on the main thread

Answer: It captures only values safe to use across concurrency domains. An @Sendable closure may only capture Sendable values, so it is safe to run concurrently.

What problem do actors solve in relation to Sendable?

  • They format strings
  • They speed up JSON
  • They replace structs
  • They isolate mutable state so it is safe to share concurrently

Answer: They isolate mutable state so it is safe to share concurrently. An actor isolates its mutable state, providing a safe way to share reference-type state across tasks.

What does @MainActor guarantee about annotated code?

  • It runs on the main actor, typically for UI work
  • It is private
  • It never suspends
  • It runs on a background thread

Answer: It runs on the main actor, typically for UI work. @MainActor pins code to the main actor, which is where UI updates must happen.

What does the nonisolated keyword do on an actor member?

  • Deletes it
  • Opts a member out of the actor's isolation so it needs no await
  • Makes it async
  • Makes it isolated

Answer: Opts a member out of the actor's isolation so it needs no await. nonisolated removes a member from actor isolation, so callers can use it synchronously without await.

Under Swift 6 strict concurrency, data races are...

  • Allowed at runtime
  • Ignored
  • Only warnings forever
  • Caught by the compiler at build time

Answer: Caught by the compiler at build time. Swift 6 makes data-race safety a compile-time guarantee, turning many races into build errors.

Why might a class NOT be automatically Sendable?

  • Classes are too fast
  • Classes cannot conform to protocols
  • It can have shared mutable reference state that risks races
  • It has no methods

Answer: It can have shared mutable reference state that risks races. A class is a reference type with potentially shared mutable state, so it is not implicitly Sendable.

How can you assert a type is Sendable when you have verified its safety manually?

  • @raw
  • @unchecked Sendable
  • @forceSendable
  • @safe

Answer: @unchecked Sendable. Conforming with @unchecked Sendable tells the compiler you guarantee safety it cannot prove, such as with an internal lock.

Crossing from one isolation domain to another with a non-Sendable value causes...

  • A compiler diagnostic under strict concurrency
  • Nothing
  • Faster code
  • Automatic copying always

Answer: A compiler diagnostic under strict concurrency. Sending a non-Sendable value across isolation boundaries is flagged by the compiler to prevent races.

Continue this course

Frequently asked questions

What is the Sendable protocol?

Sendable is a marker protocol that tells the compiler a type is safe to share across concurrency domains, meaning it can be passed between different tasks or actors without risking a data race. It carries no methods or requirements of its own; conforming is a promise about thread-safety that the compiler verifies. Value types like structs and enums built entirely from Sendable members conform automatically, while classes generally do not because they can hold shared mutable state.

What does an @Sendable closure mean?

Marking a closure @Sendable restricts what it is allowed to capture to only Sendable values, which guarantees the closure can run safely on a different task or thread without sharing mutable state unsafely. Many concurrency APIs, including the body of a Task and Task.detached, expect @Sendable closures. If you try to capture a non-Sendable reference, such as a mutable class instance, the compiler flags it, steering you toward a safe design like passing a copy or isolating the state in an actor.

How do actors and Sendable work together?

Actors are reference types that isolate their mutable state so only one task touches it at a time, which makes them a safe way to share mutable, reference-type data across concurrency domains. Because of that isolation, actor types are themselves Sendable. When you call into an actor from outside, the access is asynchronous and requires await, which is how the compiler enforces serialized, race-free access. Actors and Sendable together give you safe sharing without manual locks.

What do @MainActor and nonisolated do?

@MainActor pins a function, property, or type to the main actor, guaranteeing its code runs on the main thread, which is required for UI updates in UIKit and SwiftUI. The nonisolated keyword does the opposite for a member of an actor: it opts that member out of the actor's isolation, so it can be called synchronously without await, which is appropriate for members that only touch immutable, already-Sendable data such as a constant. Together they let you place code in exactly the right isolation domain.

What is Swift 6 strict concurrency checking?

Swift 6 turns data-race safety into a compile-time guarantee. Under strict concurrency checking, the compiler analyzes where values cross isolation boundaries and reports an error when a non-Sendable value would be shared unsafely between tasks or actors, rather than letting the bug surface as a crash or corruption at runtime. You can adopt it gradually by enabling stricter warnings in Swift 5 mode before turning on full Swift 6 mode. The payoff is that whole classes of hard-to-reproduce concurrency bugs are caught before you ever run the app.

Related lessons