Swift 6 concurrency: tasks, actors and Sendable

Task and TaskGroup, async let, actor isolation, MainActor, Sendable checking, structured cancellation and the data-race safety rules of language mode 6.

Structured concurrency

func loadDashboard(userID: Int) async throws -> Dashboard {
    // concurrent, independent pieces
    async let profile = fetchProfile(userID)
    async let orders = fetchOrders(userID)
    async let messages = fetchMessages(userID)

    return try await Dashboard(profile: profile, orders: orders, messages: messages)
}

func averageLatency(of hosts: [String]) async -> Double {
    await withTaskGroup(of: Double.self) { group in
        for host in hosts {
            group.addTask { await ping(host) }
        }
        var total = 0.0
        var count = 0.0
        for await latency in group {
            total += latency
            count += 1
        }
        return count == 0 ? 0 : total / count
    }
}

// cancellation is cooperative: check it in long loops
func process(_ items: [Item]) async throws {
    for item in items {
        try Task.checkCancellation()
        await handle(item)
    }
}
  • async let starts the child immediately; awaiting it later is when the value is collected.
  • A task group cancels all children if the parent is cancelled, which is the guarantee the old completion-handler world never had.
  • Cancellation is not preemptive — a task keeps running until it checks. Long loops must call Task.checkCancellation().
  • Task.detached inherits nothing, including priority and task-local values. It is rarely what you want.

Actors, MainActor and Sendable

actor Counter {
    private var value = 0
    func increment() -> Int {
        value += 1
        return value
    }
}

// Swift 6: a class with mutable state must be isolated or Sendable-safe
@MainActor
final class ViewModel {
    private(set) var title = ""
    private let counter = Counter()

    func refresh() async {
        title = "Loading"
        // calling an actor from a MainActor context hops off and back
        let current = await counter.increment()
        title = "Count \(current)"
    }
}

// a value type of Sendable members is Sendable automatically
struct Player: Sendable, Identifiable {
    let id: UUID
    let name: String
}

// a global mutable variable must be isolated or immutable
// in Swift 6 this is an error:
// var shared = 0
DeclarationRuns onCrossing cost
@MainActor classMain threadA hop if called from a background task
actorIts own executorAn await and possible suspension
nonisolated funcCaller's executorNone, but no isolated state
Sendable structWherever neededNone, it copies

Data-race safety in practice

// WRONG in Swift 6: the closure runs off the main actor
// and touches main-actor state without isolation
@MainActor
final class Feed {
    var items: [String] = []

    func load() async {
        let fetched = await fetchItems()
        items = fetched              // ok: still on the main actor
    }
}

// Bridging a callback API to async, keeping isolation explicit
func fetchItems() async -> [String] {
    await withCheckedContinuation { continuation in
        legacyClient.fetch { items in
            continuation.resume(returning: items)
        }
    }
}

// Adopting Swift 6 incrementally: enable strict checking per target
// .enableUpcomingFeature("StrictConcurrency")
// and fix the warnings before flipping swiftLanguageModes to .v6
⚠️
@unchecked Sendable tells the compiler to stop checking without proving anything. It is only defensible when the type wraps a lock or an actor; anywhere else it is a data race waiting for a release build to expose it.

FAQ

How do I migrate a large codebase to Swift 6 concurrency?
Turn on the upcoming features to get warnings, keep the package in Swift 5 language mode, and fix them module by module. Isolate the UI layer on @MainActor first; that alone resolves most of the diagnostics.
Do I need to mark everything Sendable?
No. Immutable structs and enums of Sendable members are inferred. You add the conformance explicitly for classes you have made safe, and the compiler then holds you to that promise.

Memory management, ARC and performance Generics, opaque types and protocol design

Last refreshed 2026-09-18.