Swift 6 Strict Concurrency
Irbisa · cheatsheetSeptember 13, 2026

Swift 6 Strict Concurrency

Isolation domains, Sendable, global actors, region isolation, sending, Swift 5 to 6 migration

Senior Developer16 itemscompressed for a skim
  1. 01

    Your app module still builds in Swift 5 mode — which concurrency flags do you turn on first, and how do you get to Swift 6 without a dead week?

    Medium

    You do not flip the language mode first: you enable the upcoming features one at a time in Swift 5 mode, where every violation is a warning and the target keeps building, and switch to Swift 6 only once the warning count is zero.

    // Package.swift — migrate one target at a time, leaves first.
    let package = Package(
      name: "App",
      targets: [
        // Done: this leaf is clean, so it opts into the language mode outright.
        .target(
    …
  2. 02

    A reviewer finds nonisolated(unsafe) in your pull request — what exactly are you promising, and when is it the honest answer rather than a silencer?

    Medium

    You are promising that you serialise every access to that declaration yourself, because the annotation only switches off the compiler's checking — it adds no lock, no queue and no runtime protection of any kind.

    import Foundation
    import Synchronization
    
    // HONEST 1 — the lock is declared next to the storage and nothing bypasses it.
    enum Metrics {
      private static let lock = NSLock()
    …
  3. 03

    Your class has nothing but let properties and the compiler still refuses to let it cross an actor boundary — what is it objecting to?

    Medium

    Sendable is never inferred for a class: you have to write the conformance, and then the class must be final, hold only immutable storage, and hold only Sendable types.

    import Foundation
    import Synchronization
    
    // Rejected, one reason each:
    // class NotFinal: Sendable { let a = 1 }
    //   -> non-final class 'NotFinal' cannot conform to the 'Sendable' protocol
    …
  4. 04

    static var shared compiled fine for years and is now an error in Swift 6 — what are the legal shapes for a piece of shared mutable state?

    Medium

    The diagnostic — "static property 'x' is not concurrency-safe because it is nonisolated global shared mutable state" — says any thread can reach that storage with nothing serialising it, so every fix makes it immutable, isolates it, or guards it.

    import Synchronization
    
    // error: static property 'current' is not concurrency-safe because it is
    //        nonisolated global shared mutable state
    // enum Session { static var current: User? }
    …
  5. 05

    You hand a plain non-Sendable model object to an actor method and it compiles — what did the compiler prove, and what makes the same line start failing?

    Medium

    It proved the value's region is disconnected: nothing outside the current scope can reach it and you never touch it again after the call, so handing it over cannot create a race even though the type is not Sendable.

    final class Report {           // not Sendable: mutable stored properties
      var title = ""
      var rows: [String] = []
    }
    
    actor Archive {
    …
  6. 06

    What can a sending parameter express that @Sendable cannot, and which compiler error does only sending fix?

    Medium

    @Sendable constrains a closure's captures — everything it closes over must already be Sendable — while sending says nothing about the type and instead transfers ownership of one value across the boundary.

    final class Draft { var body = "" }        // not Sendable
    
    actor Outbox {
      private var queued: [Draft] = []
      func queue(_ d: Draft) { queued.append(d) }
    }
    …
  7. 07

    Your @MainActor view model has to conform to Equatable and the compiler rejects the conformance — what are your options?

    Hard

    Equatable.== is nonisolated, so anyone may call it from any domain, and a main-actor witness cannot honour that without a hop it is not allowed to insert — hence "conformance of 'Model' to protocol 'Equatable' crosses into main actor-isolated code and can cause data races".

    @MainActor
    final class Model {
      let id: Int
      var rows: [String] = []
      init(id: Int) { self.id = id }
    }
    …
  8. 08

    The build goes green after you add @preconcurrency to an import and to a conformance — what did each one silence, and what risk survives the silence?

    Hard

    They are two unrelated tools that share a spelling: on an import it makes the compiler assume the module's types are Sendable, and on a conformance it replaces a compile-time isolation error with a runtime trap.

    // The SDK predates concurrency: no Sendable annotations, and its delegate
    // protocol is nonisolated.
    @preconcurrency import LegacyPlayer
    import Foundation
    
    @MainActor
    …
  9. 09

    You write a @globalActor for the storage layer, and a type that needs both it and @MainActor will not compile — what does a global actor buy you, and where does the serial-queue intuition break?

    Hard

    A global actor is one process-wide actor instance that many declarations share, and unlike two serial queues, two of them cannot nest: a declaration has exactly one isolation, so every crossing is an await and a real suspension.

    // A global actor is a type that names ONE shared actor instance.
    @globalActor
    actor StorageActor {
      static let shared = StorageActor()
    }
    …
  10. 10

    A thread-hostile C library has to be called from one context and nowhere else — how do you make an actor run its work there instead of on the cooperative pool?

    Hard

    You give the actor its own executor: implement SerialExecutor and return it from nonisolated var unownedExecutor: UnownedSerialExecutor (SE-0392), and every isolated call on that actor is enqueued there instead of on the shared pool.

    import Dispatch
    
    // 1. The cheap case: the library only needs serialisation.
    actor ImagePipeline {
      private let queue = DispatchSerialQueue(label: "image.pipeline")
      // DispatchSerialQueue already conforms to SerialExecutor.
    …
  11. 11

    The vendor documents that its callback arrives on the main thread but the method is nonisolated — is MainActor.assumeIsolated the right tool, and what makes it crash?

    Hard

    It is a runtime assertion, not a cast: assumeIsolated checks that you are already running on that actor and then lends you its isolation synchronously — it never switches, and if the check fails it traps.

    import UIKit
    
    @MainActor
    final class Player {
      private var progress: Double = 0
      private let bar = UIProgressView()
    …
  12. 12

    nonisolated on a stored property, on a synchronous method and on an async method — what does each promise, and which of them puts work in the background?

    Hard

    None of them moves work anywhere: nonisolated removes a declaration from its actor's domain, so the only thing it changes is who may call it without await.

    @MainActor
    final class Profile: Equatable, Hashable {
      let id: UUID                 // Sendable `let`: already readable cross-domain
      var displayName: String      // main-actor isolated
    
      init(id: UUID, name: String) { self.id = id; displayName = name }
    …
  13. 13

    What do Approachable Concurrency and Default Actor Isolation: MainActor change in an app target, and why would you not ship a shared framework with them on?

    Hard

    defaultIsolation (SE-0466) flips the default for unannotated declarations in a module from nonisolated to @MainActor, which deletes most of a UI target's Sendable errors — and because that default lands on your public API, a framework imposes it on everyone who imports it.

    // Package.swift — swift-tools-version: 6.2
    //
    // .target(
    //   name: "AppUI",
    //   swiftSettings: [
    //     .defaultIsolation(MainActor.self),      // the target that owns the UI
    …
  14. 14

    A vendor's Objective-C SDK calls your delegate back on whatever thread it likes — how do you get that into a Swift 6 module without @unchecked Sendable on everything?

    Hard

    Put one small nonisolated object between the SDK and your code and make it the only thing the vendor's threads ever touch: its delegate methods own no mutable state, and they hand every event to an actor through a Sendable channel.

    import Foundation
    
    // ---- The boundary: nonisolated, owns nothing mutable, therefore Sendable. ----
    final class ScannerBridge: NSObject, VendorScannerDelegate {
      enum Event: Sendable { case found(String), failed(String) }
    …
  15. 15

    Your module compiles clean in Swift 6 language mode and still corrupts data under load — which races is the isolation checker structurally unable to see?

    Hard

    The checker reasons about Swift values crossing isolation boundaries; sharing that happens through an address, through another language, or through the kernel is outside the theorem it proves.

    import Foundation
    import Synchronization
    
    // 1. Pointers are Sendable, so this compiles in Swift 6 mode — and races.
    func corrupt(_ buffer: UnsafeMutablePointer<Int>, count: Int) {
      Task.detached { for i in 0..<count { buffer[i] += 1 } }
    …
  16. 16

    Thread Sanitizer reports a race in a target that compiles clean under Swift 6 — is the compiler wrong, and how do you track the bug down?

    Hard

    Neither is wrong: the compiler proves that no unchecked value crosses an isolation boundary in the code it was allowed to check, while TSan watches real memory accesses at runtime, including everything the compiler was told to skip.

    // Compiles clean in Swift 6 mode. TSan reports a race on `pending`.
    final class UploadQueue: @unchecked Sendable {   // the check was waived here
      private var pending: [URL] = []
      private let lock = NSLock()
    
      func add(_ url: URL) {
    …