Skip to main content

Swift 6 Strict Concurrency: 11 Sendable and MainActor Errors

Swift 6 strict concurrency turns data races into errors. Fix 11 common errors around Sendable, @MainActor and global state, then migrate step by step.

Swift 6 strict concurrency errors have predictable fixes. The Swift 6 language mode turns data race checks from warnings into errors, and most errors you hit after upgrading fall into three groups: non-Sendable values crossing isolation domains, @MainActor state accessed from a nonisolated context, and global or static mutable variables. Put UI code on @MainActor, make data that crosses threads a value type or Sendable, and move shared mutable state into an actor. This article lists 11 common errors with their exact text, the cause and the fix in code, then shows how to migrate step by step in Swift 5 mode.

The error text comes from the Swift 6.4 compiler. Other versions word some messages slightly differently, so match on the keywords. A few system APIs carry @preconcurrency annotations, so some of these currently show up as warnings in Swift 6 mode. The problems are real and still worth fixing. For the full background, read the Swift 6 concurrency migration guide on swift.org and Apple’s Adopting strict concurrency in Swift 6 apps.

What do isolation domain, Sendable and crossing a boundary mean?

  • Isolation domain. A region that only one piece of code can access at a time. @MainActor is the main thread’s domain, every actor instance is its own domain, and unannotated code is nonisolated and can run on any thread.
  • Sendable. A type that can safely move from one isolation domain to another. Non-public structs and enums whose members are all Sendable are Sendable automatically, and so are actors and classes marked @MainActor. A plain class isn’t Sendable by default.
  • Crossing a boundary. A value moving from one domain to another, for example into an actor method, or a main-thread object used inside a background closure. The compiler only allows it when it can prove it’s safe.

UIViewController, UIView and SwiftUI’s View are already @MainActor, so your subclasses and the code in body run in the main actor’s domain. Many errors come from carrying one of these main-thread objects into a background closure.

1. A static var isn’t concurrency-safe global state

1static property 'shared' is not concurrency-safe because it is nonisolated global shared mutable state

Any thread can read and write a static var or a global var, so the compiler can’t rule out a data race.

1final class AppSettings {
2    static var shared = AppSettings()
3    var apiBase = "https://api.example.com"
4}

If the singleton is only used by UI code, put the whole class on the main actor and change var to let.

1@MainActor
2final class AppSettings {
3    static let shared = AppSettings()
4    var apiBase = "https://api.example.com"
5}

If it only holds constants, store a Sendable value with let.

1enum API {
2    static let base = URL(string: "https://api.example.com")!
3}

If you write static let shared = AppSettings() and the class isn’t Sendable, you get a different error, static property 'shared' is not concurrency-safe because non-'Sendable' type 'AppSettings' may have shared mutable state. The fix is the same. If you really do protect every access with a lock or a queue, you can write nonisolated(unsafe) static var. The compiler stops checking it, and safety is your responsibility.

2. How do you access a main actor property from a nonisolated context?

1main actor-isolated property 'count' can not be referenced from a nonisolated context

count belongs to the main actor, while badgeText is a plain function with no isolation that could be called from any thread.

1@MainActor
2final class CartModel {
3    var count = 0
4}
5
6func badgeText(for cart: CartModel) -> String {
7    "\(cart.count)"
8}

If only UI code calls this function, mark it @MainActor too.

1@MainActor
2func badgeText(for cart: CartModel) -> String {
3    "\(cart.count)"
4}

When the code writes to the property, referenced in the error becomes mutated. The fix is the same.

3. Reading a main actor property directly in an async function

1main actor-isolated property 'count' cannot be accessed from outside of the actor

Older compilers report expression is 'async' but is not marked with 'await'. To read a main actor property from a nonisolated async function, the code has to hop to the main actor first. That hop is asynchronous, so you need await.

1func makeRequest(cart: CartModel) async -> URLRequest {
2    var request = URLRequest(url: URL(string: "https://api.example.com/cart")!)
3    request.setValue("\(cart.count)", forHTTPHeaderField: "X-Count")
4    return request
5}

await the value first, then use it.

1func makeRequest(cart: CartModel) async -> URLRequest {
2    let count = await cart.count
3    var request = URLRequest(url: URL(string: "https://api.example.com/cart")!)
4    request.setValue("\(count)", forHTTPHeaderField: "X-Count")
5    return request
6}

4. Calling a main actor method from a callback

1call to main actor-isolated instance method 'show' in a synchronous nonisolated context

URLSession’s completion handler is a @Sendable closure that runs on a background thread, so it can’t call a view controller method synchronously. This one is currently a warning, but updating UI from a background thread is a real bug. Most places in older code that wrap work in DispatchQueue.main.async belong to this group.

 1final class FeedViewController: UIViewController {
 2    func show(_ data: Data) {
 3        // Update the UI
 4    }
 5
 6    func fetch(_ url: URL) {
 7        URLSession.shared.dataTask(with: url) { data, _, _ in
 8            guard let data else { return }
 9            self.show(data)
10        }.resume()
11    }
12}

Switch to async/await. The method itself runs on the main actor, the network request runs in the background during await, and execution continues on the main actor when it returns. data(from:) is available from iOS 15.

 1final class FeedViewController: UIViewController {
 2    override func viewDidLoad() {
 3        super.viewDidLoad()
 4        Task {
 5            await fetch(URL(string: "https://api.example.com/feed")!)
 6        }
 7    }
 8
 9    func show(_ data: Data) {
10        // Update the UI
11    }
12
13    func fetch(_ url: URL) async {
14        do {
15            let (data, _) = try await URLSession.shared.data(from: url)
16            show(data)
17        } catch {
18            print("Request failed: \(error)")
19        }
20    }
21}

If you don’t want to rewrite the callback yet, write Task { @MainActor in self.show(data) } inside the closure. Data is Sendable, so it can come along.

5. A protocol conformance that crosses isolation (usually a delegate)

1conformance of 'PlayerViewController' to protocol 'PlayerDelegate' crosses into main actor-isolated code and can cause data races

Swift 6.0 words it as main actor-isolated instance method 'playerDidFinish()' cannot be used to satisfy nonisolated protocol requirement. The protocol has no isolation, so playerDidFinish() could be called from any thread, but the view controller implementing it lives on the main actor.

 1protocol PlayerDelegate: AnyObject {
 2    func playerDidFinish()
 3}
 4
 5final class PlayerViewController: UIViewController, PlayerDelegate {
 6    private var statusText = ""
 7
 8    func playerDidFinish() {
 9        statusText = "Playback finished"
10    }
11}

Pick the option that fits your case.

  • You own the protocol and callbacks always arrive on the main thread. Mark the protocol @MainActor. This is the cleanest fix.
1@MainActor
2protocol PlayerDelegate: AnyObject {
3    func playerDidFinish()
4}
  • Swift 6.2 and later. Declare a conformance that only holds on the main actor, without changing the protocol.
1final class PlayerViewController: UIViewController, @MainActor PlayerDelegate {
2    private var statusText = ""
3
4    func playerDidFinish() {
5        statusText = "Playback finished"
6    }
7}
  • A system or third-party protocol whose documentation says callbacks arrive on the main thread. Mark the implementation nonisolated and get back onto the main actor with MainActor.assumeIsolated. If it’s ever called off the main thread, the app crashes right there, which surfaces the problem early.
1nonisolated func playerDidFinish() {
2    MainActor.assumeIsolated {
3        self.statusText = "Playback finished"
4    }
5}
  • Callbacks may come from a background thread. Mark the implementation nonisolated and switch back with Task { @MainActor in ... } inside it.
  • A third-party protocol that hasn’t adopted concurrency yet. Put @preconcurrency before the conformance, as in UIViewController, @preconcurrency PlayerDelegate. The compile error turns into a runtime check.

6. Why does passing a non-Sendable value into an actor fail?

1sending 'draft' risks causing data races

Draft is a plain class. After you hand it to the actor, both your code and the actor hold the same object. The print line after it still uses the object, so the compiler reports an error.

 1final class Draft {
 2    var text = ""
 3}
 4
 5actor DraftStore {
 6    private var drafts: [Draft] = []
 7
 8    func save(_ draft: Draft) {
 9        drafts.append(draft)
10    }
11}
12
13func submit(to store: DraftStore) async {
14    let draft = Draft()
15    draft.text = "Hello"
16    await store.save(draft)
17    print(draft.text)
18}

Make the data model a struct. A non-public struct with only Sendable members is Sendable automatically, and the actor receives a copy.

1struct Draft {
2    var text = ""
3}

If you can’t switch to a struct yet, stop using the object after passing it to the actor. Delete the print that follows and the error goes away. A public struct isn’t inferred as Sendable, so declare : Sendable explicitly.

7. A @Sendable closure captures a non-Sendable self

1capture of 'self' with non-Sendable type 'ThumbnailCache' in a '@Sendable' closure

The closure passed to DispatchQueue.global().async is @Sendable. This one is currently a warning, but several background closures changing the images dictionary at the same time is a real data race.

 1final class ThumbnailCache {
 2    private var images: [URL: Data] = [:]
 3
 4    func preload(_ url: URL) {
 5        DispatchQueue.global().async {
 6            if let data = try? Data(contentsOf: url) {
 7                self.images[url] = data
 8            }
 9        }
10    }
11}

Turn a cache shared across threads into an actor. Access to images is then serialized for you.

 1actor ThumbnailCache {
 2    private var images: [URL: Data] = [:]
 3
 4    func image(for url: URL) async throws -> Data {
 5        if let cached = images[url] {
 6            return cached
 7        }
 8        let (data, _) = try await URLSession.shared.data(from: url)
 9        images[url] = data
10        return data
11    }
12}

8. Mutating a captured variable in concurrent closures

1mutation of captured var 'finished' in concurrently-executing code

Several background closures run += 1 on one local variable at the same time, so the result is unpredictable. This one is also currently a warning.

1func uploadAll(_ files: [URL]) {
2    var finished = 0
3    for file in files {
4        DispatchQueue.global().async {
5            upload(file)
6            finished += 1
7        }
8    }
9}

Use a task group instead. Child tasks only return results, and you count them when you collect the results.

 1func uploadAll(_ files: [URL]) async -> Int {
 2    await withTaskGroup(of: Bool.self) { group in
 3        for file in files {
 4            group.addTask { await upload(file) }
 5        }
 6        var finished = 0
 7        for await ok in group where ok {
 8            finished += 1
 9        }
10        return finished
11    }
12}
13
14func upload(_ file: URL) async -> Bool {
15    // Upload the file and return true on success
16    true
17}

9. How do you update main actor state from Task.detached?

1main actor-isolated property 'items' can not be mutated from a nonisolated context

Task.detached doesn’t inherit the current isolation. Its closure runs in the background and can’t write main actor properties directly.

 1@MainActor
 2final class FeedModel {
 3    var items: [String] = []
 4
 5    func reload(_ data: Data) {
 6        Task.detached {
 7            let parsed = FeedModel.parse(data)
 8            self.items = parsed
 9        }
10    }
11
12    nonisolated static func parse(_ data: Data) -> [String] {
13        // Slow parsing
14        []
15    }
16}

Let the background task only compute the result, wait for it with .value, and assign it back on the main actor.

1func reload(_ data: Data) async {
2    let parsed = await Task.detached(priority: .userInitiated) {
3        FeedModel.parse(data)
4    }.value
5    items = parsed
6}

From Swift 6.2 you can mark the parsing function @concurrent. It always runs on the background thread pool, and the caller just awaits it with no Task.detached.

1func reload(_ data: Data) async {
2    items = await FeedModel.parse(data)
3}
4
5@concurrent
6static func parse(_ data: Data) async -> [String] {
7    []
8}

The difference between Task { } and Task.detached { } is that the first inherits the current isolation. A Task { } created in a view controller still runs on the main thread, so slow work inside it freezes the UI.

10. How do you fix a mutable property in a Sendable class?

1stored property 'value' of 'Sendable'-conforming class 'Counter' is mutable

If you add Sendable to a class to silence some other error, the compiler checks whether it really is safe.

1final class Counter: Sendable {
2    var value = 0
3}

Make it an actor.

1actor Counter {
2    private(set) var value = 0
3
4    func increment() {
5        value += 1
6    }
7}

If you need synchronous calls and your minimum version is iOS 18, use Mutex from the Synchronization framework.

 1import Synchronization
 2
 3@available(iOS 18.0, *)
 4final class Counter: Sendable {
 5    private let value = Mutex(0)
 6
 7    func increment() {
 8        value.withLock { $0 += 1 }
 9    }
10
11    var current: Int {
12        value.withLock { $0 }
13    }
14}

On older versions, if every access is already protected by an NSLock, you can declare the class @unchecked Sendable and take responsibility for its safety.

11. A main actor type used as a nonisolated default value

1main actor-isolated default value in a nonisolated context

ProfileViewModel defines its own init, so that initializer belongs to the main actor. AppDependencies has no isolation, so its property default values could be evaluated on any thread.

 1@MainActor
 2final class ProfileViewModel {
 3    var name: String
 4
 5    init(name: String = "") {
 6        self.name = name
 7    }
 8}
 9
10final class AppDependencies {
11    let profile = ProfileViewModel()
12}

If AppDependencies is only used by UI code, mark it @MainActor too. If the initializer only assigns Sendable properties and touches no other main actor state, you can also mark the init as nonisolated.

1@MainActor
2final class ProfileViewModel {
3    var name: String
4
5    nonisolated init(name: String = "") {
6        self.name = name
7    }
8}

A main actor class with no explicit init and only simple default values gets a compiler-generated initializer that’s already nonisolated, so it doesn’t trigger this error.

How do you look up a Swift 6 strict concurrency error quickly?

Error keywords Cause Preferred fix
is not concurrency-safe because it is nonisolated global shared mutable state Global or static var Change to let, or mark @MainActor
non-'Sendable' type ... may have shared mutable state static let holds a non-Sendable object Mark the type @MainActor or make it Sendable
can not be referenced from a nonisolated context Plain function reads a main actor property Mark the function @MainActor
cannot be accessed from outside of the actor / expression is 'async' but is not marked with 'await' Async function reads a property from another domain Add await
call to main actor-isolated ... in a synchronous nonisolated context Background callback calls a main actor method Switch to async/await, or Task { @MainActor in }
crosses into main actor-isolated code / cannot be used to satisfy nonisolated protocol requirement Delegate protocol has no isolation Mark the protocol @MainActor, or use a @MainActor conformance
sending '...' risks causing data races Non-Sendable object still used after passing it to an actor Make it a struct, or stop using it after passing it
capture of 'self' with non-Sendable type Background closure captures a plain class Use an actor or async/await
mutation of captured var ... in concurrently-executing code Concurrent closures change a local variable Collect results with a task group
can not be mutated from a nonisolated context (inside Task.detached) Background task writes main actor state directly Get the result with .value and assign it, or use @concurrent
stored property ... of 'Sendable'-conforming class ... is mutable Sendable class has a var Use an actor or Mutex
main actor-isolated default value in a nonisolated context Nonisolated type creates a main actor object Mark the outer type @MainActor, or use nonisolated init

How do you migrate an existing project to Swift 6 step by step?

Switching an older project straight to Swift 6 can produce hundreds of errors. The order the official migration guide recommends is to stay in Swift 5 mode and turn the checks up one level at a time. Problems show up as warnings, and the project keeps building and running.

  1. Stay in Swift 5 mode and turn on strict concurrency checking. The SWIFT_STRICT_CONCURRENCY build setting (Strict Concurrency Checking) has three levels, minimal, targeted and complete. Start with targeted, clear the warnings, then move to complete.
  2. Migrate one module at a time. Start with the app target, which no other module depends on, or first fix the global variables and data models in a shared module used everywhere. Either kind of change clears a large batch of warnings at once.
  3. Annotate what’s true first, refactor later. The migration guide’s principle is to express what is true now. Describe the current state with @MainActor, nonisolated and @preconcurrency. Use nonisolated(unsafe) and @unchecked Sendable sparingly, and treat every place you use them as a to-do list for later refactoring.
  4. Switch to Swift 6. Once the complete level shows no warnings, set the module’s SWIFT_VERSION to 6. Other modules can stay on 5, and mixing both in one project works fine.

Swift 6.2 adds several settings that make code more single-threaded by default. The setting names and descriptions in this table come from Apple’s Build settings reference.

Build setting Values Effect
SWIFT_DEFAULT_ACTOR_ISOLATION (Default Actor Isolation) nonisolated (default) or MainActor With MainActor, unannotated code is inferred as @MainActor. A good fit for app targets
SWIFT_APPROACHABLE_CONCURRENCY (Approachable Concurrency) YES / NO Turns on several new features at once, including NonisolatedNonsendingByDefault and InferIsolatedConformances
SWIFT_UPCOMING_FEATURE_NONISOLATED_NONSENDING_BY_DEFAULT YES / NO Nonisolated async functions run in the caller’s isolation domain by default. Mark code that should run in the background with @concurrent
SWIFT_UPCOMING_FEATURE_INFER_ISOLATED_CONFORMANCES YES / NO Protocol conformances of main actor types are inferred as @MainActor conformances, which removes many of the errors in section 5

SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor saves a lot of work in UI-heavy apps. Most of the errors in sections 2, 3 and 11 disappear. Mark networking, parsing and other code that should run in the background with nonisolated or @concurrent. These settings need Swift 6.2 or later. Run swift --version in a terminal to check your compiler.

For Swift Package targets, set the equivalents in Package.swift. defaultIsolation needs swift-tools-version 6.2 or later.

 1// swift-tools-version: 6.2
 2import PackageDescription
 3
 4let package = Package(
 5    name: "MyKit",
 6    targets: [
 7        // Migrated, uses the tools version's default Swift 6 mode
 8        .target(name: "Networking"),
 9        // Still migrating, stays in Swift 5 mode with full checking on
10        .target(
11            name: "LegacyUI",
12            swiftSettings: [
13                .swiftLanguageMode(.v5),
14                .enableUpcomingFeature("StrictConcurrency")
15            ]
16        ),
17        // UI module, main actor isolation by default
18        .target(
19            name: "Features",
20            swiftSettings: [
21                .defaultIsolation(MainActor.self)
22            ]
23        )
24    ]
25)

How do you change the concurrency settings in KXBuild?

When you use KXBuild on Windows or Linux, all of these are ordinary build settings in the project. Change them in the Project Settings Editor, described in Project settings.

  • Swift version. In the KXBuild view, right-click the target and choose KXBuild: Edit Target Settings. Swift Language Version in the Build section of the General page is SWIFT_VERSION.
  • Concurrency settings. Switch to the Build Settings page, choose All and type CONCURRENCY or ACTOR_ISOLATION in the search box. Turn on “Show setting names next to titles” to see the setting names from the table above. Quick Help at the bottom shows the allowed values and a description. The Levels view shows whether the current value comes from the target, the project or an xcconfig file.
  • Save. Press Ctrl+S to write project.pbxproj. Only the lines you changed are touched.

To see how many warnings full checking produces without editing the project file, override the build setting for one build with -D.

1kxbuild build .\MyApp.xcodeproj -D SWIFT_STRICT_CONCURRENCY=complete -D CODE_SIGNING_ALLOWED=NO

The full log is in Output > KXBuild in VS Code. Strict concurrency errors and warnings appear in the Problems panel, and clicking one jumps to the line. See Build and configuration for more on -D.

FAQ

Do I have to switch to Swift 6 after upgrading Xcode or the Swift compiler?

No. New compilers still support the Swift 5 language mode, and with SWIFT_VERSION set to 5 these checks are at most warnings. You can migrate one module at a time, and Swift 5 and Swift 6 targets can live in the same project.

Can I silence the errors with @unchecked Sendable or nonisolated(unsafe)?

Only if you really protect every access with a lock or a serial queue. They only stop the compiler from checking. The data race is still there, and if you get it wrong your users see random crashes or corrupted data. Prefer @MainActor, an actor or a struct.

What’s the difference between Task { } and Task.detached { }?

Task { } inherits the current isolation, so one created in a view controller runs on the main thread. Task.detached { } doesn’t inherit it, runs in the background and can’t touch main actor state directly. To move slow work to the background, return the result from Task.detached and assign it afterwards, or from Swift 6.2 use a @concurrent function.

What happens when I set SWIFT_DEFAULT_ACTOR_ISOLATION to MainActor?

Every unannotated type and function in the module defaults to the main actor, and most concurrency errors in UI code disappear. The trade-off is that slow work also runs on the main thread by default, so functions that belong in the background need an explicit nonisolated or @concurrent. This setting needs Swift 6.2 or later.

How do I fix a delegate conformance that crosses into main actor-isolated code?

If you own the protocol and callbacks always arrive on the main thread, mark the protocol @MainActor. From Swift 6.2 you can also write an isolated conformance such as @MainActor PlayerDelegate. For system or third-party protocols, mark the method nonisolated and get back to the main actor with MainActor.assumeIsolated or Task { @MainActor in }.