What Is Swift 6 Strict Concurrency Checking?
The short answer
Strict concurrency checking is the Swift compiler's data-race checker. It verifies that mutable state is only touched by its owner, and that values crossing isolation boundaries (between actors, tasks or the main actor) are Sendable or safely handed over. In Swift 5 mode, you choose how much of it runs: Minimal, Targeted or Complete, with findings reported as warnings. In the Swift 6 language mode, complete checking is always on, and findings are errors.
Setting Checks Reported as
─────────────────────────────── ───────────────────────────── ───────────
Swift 5 mode, Minimal (default) explicit annotations only errors/warnings for those
Swift 5 mode, Targeted + code that adopted concurrency warnings
Swift 5 mode, Complete whole module warnings
Swift 6 mode whole module errors
The misconception is that strict concurrency checking is a runtime tool, like Thread Sanitizer, or that it makes your code slower. It runs at compile time and adds nothing to your app. It's a static proof: if the checker can't prove a piece of code is race-free, it tells you before you ship.
💡 Important Terminology
Data race = two threads accessing the same memory at once, with at least one writing. Undefined behaviour, and the source of rare, unreproducible crashes.
Isolation = who owns a piece of state: an actor, the main actor, or nobody.
Sendable checking = verifying that values crossing an isolation boundary are safe to share.
The Mental Model: A Building Inspector Who Reads the Plans
Thread Sanitizer is a smoke detector: it goes off when something actually burns while you're watching. Strict concurrency checking is an inspector who reads the plans and refuses to sign off on wiring that could catch fire.
source code ──► strict concurrency checking (compile time) ──► warnings / errors
app running ──► Thread Sanitizer (runtime, debug builds) ──► reports races it sees
Step 1: See the Levels on Real Code
We compiled one file with Swift 6.2 at every level. It contained two problems: a non-Sendable class captured in a @Sendable closure, and a non-Sendable value sent to an actor and then used again.
Swift 5, Minimal: 0 diagnostics.
Swift 5, Targeted: 0 diagnostics.
Swift 5, Complete: 2 warnings, each saying "this is an error in the Swift 6 language mode".
Swift 6 mode: 1 error. The second problem only appeared once the first was fixed.
Swift 6 mode with -strict-concurrency=minimal: still 1 error. You can't turn checking down inside Swift 6 mode.
Step 2: Notice What's Checked at Every Level
One thing was an error at every level, including Swift 5 Minimal: mutating a property of an explicitly @MainActor class from a nonisolated function. The message was "main actor-isolated property 'n' can not be mutated from a nonisolated context". Explicit annotations have always been enforced. The levels control how far the checker goes into code without annotations.
Step 3: Read the Three Main Kinds of Finding
Almost every strict-concurrency message falls into one of three groups:
1. Isolation "actor-isolated property … can not be mutated from a nonisolated context"
→ call through the owner, with await
2. Sendable "capture of 'feed' with non-Sendable type 'LaunchFeed' in a '@Sendable' closure"
→ make the type Sendable, or don't share it
3. Global state "static property 'current' is not concurrency-safe because it is
nonisolated global shared mutable state"
→ give it an owner, or make it an immutable let
What Is Actor Isolation in Swift? and What Is Sendable in Swift? explain the first two in depth.
Step 4: See What Passing Code Looks Like
CFA code is shaped to pass complete checking. Here's a feature manager that compiled in Swift 6 mode with no warnings:
// 2 - AppModel/Features/LaunchSearch/LaunchSearchManager.swift
@MainActor
@Observable
final class LaunchSearchManager {
private(set) var results: [Launch] = []
private(set) var errorMessage: String?
private let provider: any LaunchesProvider // Sendable protocol
private var searchTask: Task<Void, Never>?
init(provider: any LaunchesProvider) { self.provider = provider }
func search(_ query: String) {
searchTask?.cancel()
searchTask = Task { // inherits the main actor
do {
let launches = try await provider.fetchLaunches(matching: query)
guard !Task.isCancelled else { return }
results = launches // [Launch] is Sendable
} catch {
guard !Task.isCancelled else { return }
errorMessage = "Search failed."
}
}
}
}
Every piece of state has one owner, the main actor. The only value that crosses a boundary, [Launch], is a Sendable array of structs.
Step 5: Understand What It Can't Catch
The checker proves the absence of data races. It doesn't prove your logic is right. Two main-actor tasks can still finish in the wrong order and show stale data. That's a logical race, and you handle it with cancellation, as searchTask?.cancel() does above, and with tests. And @unchecked Sendable or nonisolated(unsafe) switch the checker off for that code, so every use deserves a comment.
Step 6: Watch Out for Hidden Findings
Some checks, such as region-based "sending" checks, run late in compilation. In our tests, swiftc -typecheck didn't report them, and a type error elsewhere could hide them until it was fixed. When you measure a codebase, do full builds and recount after fixes.
Targeted or Complete?
Targeted was designed as a stepping stone: it checks code that has already adopted concurrency features, without flagging every legacy class in the module. In our small test it found nothing that Complete found, which is typical of code that mixes callbacks and a little async. For a migration, Complete is the more useful setting, because it shows the same set of problems Swift 6 mode will report, as warnings you can work through. If Complete produces too many warnings to act on, don't drop back to Targeted. Split the work by feature and fix one slice at a time, so the warning count falls steadily instead of being hidden.
How Teams Use It in a Migration
Most teams turn on Complete in Swift 5 mode, fix warnings feature by feature, then switch each target to Swift 6 mode. How Do I Enable Strict Concurrency in Swift 6? has the exact settings. If you want AI help, the free CFA AI Toolkit includes a Swift Concurrency migration skill that records behaviour first and migrates one feature at a time. Metro Mate shows the result, with the original app on main and the migrated app on the migration/cfa-toolkit-swift-concurrency branch.
Common Mistakes
Treating warnings at Complete as optional. They're Swift 6 errors in waiting.
Expecting -strict-concurrency=minimal to relax Swift 6 mode. It doesn't.
Assuming a clean build means correct ordering. Logical races need tests.
Measuring with -typecheck. Use full builds.
Related Articles
Start with the pillar, What Is a Task in Swift?, then read the rest of this cluster:
About Us

3 Days of Swift Concurrency is created by Matthew Thomas, an iOS developer who has been building in public since 2022. He publishes the free, MIT-licensed CFA Toolkit and a set of public reference apps on GitHub. The paid training programme is still being built: a soft launch is targeted for 1 December 2026, but that date depends on deadlines being met and isn't guaranteed.




Comments