Swift 6.2 Concurrency in Practice: Default to MainActor, Escape on Purpose

Swift 6.0 made data races a compile error and, for a year, made everyone pay for it. The strict-concurrency checker turned ordinary UI code into a wall of Sendable violations and “main actor-isolated property can not be referenced from a nonisolated context” errors. The diagnosis was correct (that code really could race), but the volume buried the signal, and a lot of teams either stayed on Swift 5 mode or sprinkled @MainActor until the errors went quiet.

Swift 6.2 changes the default instead of the rules. The data-race safety guarantee is the same; what changed is where the compiler starts. Adopt the new defaults and most of the wall disappears, because the compiler now assumes what was already true of your app: most of your code runs on the main actor, and you leave it on purpose, in named places. This model is the one I ship across the 941-family apps1, and it is one piece of a larger evolution arc that the What’s New in Swift roundup for 2026 traces through the 6.3 and 6.4 proposals that followed. Here is how it works, and the six errors that still bite once you turn it on.

TL;DR

  • SE-0466 lets you default an entire module to @MainActor. Set SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor and stop annotating every view, model, and view-model2.
  • SE-0461 makes nonisolated async functions run on the caller’s actor by default (nonisolated(nonsending)), so calling async code no longer forces an actor hop and the Sendable-across-the-boundary errors that came with it3.
  • @concurrent is the escape hatch. Mark a function @concurrent to push heavy CPU work (decoding, image processing) onto a background thread, deliberately and visibly4.
  • Xcode 26 turned both defaults on for new projects, and Xcode 27, now final with Swift 6.4, keeps them in its templates unchanged. The umbrella switch is SWIFT_APPROACHABLE_CONCURRENCY = YES5.
  • The model inverts the burden: instead of proving every line is safe to run off-main, you keep everything on-main and prove the few places you leave.
  • Six concrete error patterns survive the switch. All six have one-line fixes once you see the pattern, and the newest fires inside the escape hatch itself: @concurrent code cannot use a main-actor-isolated Codable conformance until you mark the conformance nonisolated12.

The inversion, and why it matters

Old model (Swift 6.0): code is nonisolated until you isolate it. Every type that touched UI state needed @MainActor, every async call across an isolation boundary needed Sendable conformance, and the compiler flagged each gap. For an app where 95 percent of the code already runs on the main thread, you spent your time annotating the 95 percent to describe a fact that was never in doubt.

New model (Swift 6.2): code is on the main actor until you leave it. SE-0466 lets you declare main-actor isolation as the module default, so a view, its model, and its helpers are all @MainActor without a single annotation2. SE-0461 then removes the second tax: a nonisolated async function now runs on whatever actor called it instead of hopping to the global executor, so awaiting it does not drag you across an isolation boundary or demand Sendable on everything in scope3.

The mental model is the one that matches how UI apps actually behave. Main-thread by default is not a compromise; it is the truth of an app whose state is its views. Concurrency becomes the exception you reach for, named and contained, rather than the ambient condition you defend against on every line. The compiler’s job flips from “prove this is safe to run concurrently” to “you said this runs concurrently, so prove it is safe,” and the second question is asked in far fewer places.

The Swift core team has said as much. At the WWDC 2026 Swift Group Lab, Holly described how, designing the concurrency model today, nonisolated async functions would have run in the caller’s context from day one; paraphrased, she said she had held on to the global-concurrent-pool model for a long time and was convinced otherwise by issues in real-world projects. Doug added that the old default pushed too many types toward Sendable. (Both quotes are paraphrased from a locally transcribed lab recording; Apple published no captions for the session.)9

Turning it on

Two build settings, both introduced as defaults for new projects in Xcode 26, both kept unchanged in the templates of Xcode 27 now that the release is final with Swift 6.4, and both worth setting explicitly on an existing project5:

SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor
SWIFT_APPROACHABLE_CONCURRENCY = YES

The first is SE-0466: the module defaults to main-actor isolation. The second is the umbrella that enables the approachable-concurrency feature set: five upcoming features, DisableOutwardActorInference, GlobalActorIsolatedTypesUsability, InferIsolatedConformances, InferSendableFromCaptures, and NonisolatedNonsendingByDefault, the last of which is the SE-0461 caller-runs behavior5. The list carries a precision worth stating now that Swift 6.4 has shipped: bare Swift 6 language mode still runs nonisolated async functions on the global executor, because NonisolatedNonsendingByDefault stays gated behind its upcoming-feature flag rather than becoming the language default3. You get the caller-runs behavior from the build setting, not from the compiler version. In a Swift package, SE-0466 defines a dedicated setting for the first half, SwiftSetting.defaultIsolation(MainActor.self), and you pair it with .enableUpcomingFeature("NonisolatedNonsendingByDefault") for the second6.

Flip both on an existing project and the error count drops hard, because most of what the checker was flagging was main-thread code it could not previously assume was main-thread. What remains is a short list of genuine boundary cases. They are worth knowing by name, because each one is a place where your code really does leave the main actor, and the fix is to say so precisely.

The six errors that survive the switch

These are the strict-concurrency error patterns that still appear under SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, taken from migrating the 941-family apps to the model1. Each is a real boundary, not a false positive, which is why the fix is a precise annotation rather than a suppression.

1. Pure functions on Sendable types. A pure method on a Sendable enum (a URL builder, a formatter) inherits main-actor isolation under the module default, then errors when called from a nonisolated context: “Call to main actor-isolated instance method ‘searchURL(for:)’ in a synchronous nonisolated context.” Swift 6.4 stamps the message with a diagnostic-group tag, [#ActorIsolatedCall], which links to the compiler’s own explanation page10. The method touches no state, so isolating it to the main actor is wrong. Mark it nonisolated7:

nonisolated func searchURL(for query: String) -> URL? { ... }

2. Singleton statics in default parameters. static let shared = Foo() is main-actor-isolated under the default, but default-parameter values are evaluated in the caller’s context, which is often nonisolated: “Main actor-isolated default value in a nonisolated context.” Reference the same static from a nonisolated function body rather than a default argument and the compiler words it the older way: “Main actor-isolated static property ‘shared’ can not be referenced from a nonisolated context.”13 Make the static nonisolated. If the type is Sendable (or @unchecked Sendable because you guard its state yourself), you need no unsafe qualifier:

final class KeychainProxySecretStore: @unchecked Sendable {
    nonisolated static let shared = KeychainProxySecretStore()
}

3. Immutable primitive constants. The same default-parameter problem hits a plain constant: a static let defaultInterval referenced from a nonisolated default argument. The fix is identical and the constant is trivially safe to share:

nonisolated static let defaultInterval: TimeInterval = 15 * 60

4. A Task body reading captured self. An outer closure captures [weak self]; inside, a Task { @MainActor in self?.foo() } reads that captured optional: “Reference to captured var ‘self’ in concurrently-executing code.” Swift 6.2 treats the weak capture as a mutable binding read concurrently and flag it; Swift 6.4 in Xcode 27 understands the capture and compiles the same shape clean13. On the older toolchains, re-capture self at the Task boundary so the Task owns an immutable binding:

NotificationCenter.default.addObserver(...) { [weak self] _ in
    Task { @MainActor [weak self] in
        self?.value = next
    }
}

The diagnostic itself did not retire. Its Swift 6.4 form carries the [#SendableClosureCaptures] tag, and it still fires wherever a @Sendable closure captures a local var that the enclosing scope goes on to mutate, which is the race the checker always meant: “Reference to captured var ‘count’ in concurrently-executing code.”13

nonisolated func f() {
    var count = 0
    let c: @Sendable () -> Void = { _ = count }
    count = 1
    c()
}

5. KVO callbacks reading main-actor state. A webView.observe(\.canGoBack) { wv, _ in ... } callback is @Sendable and therefore nonisolated, but WKWebView.canGoBack is main-actor-isolated: “Main actor-isolated property ‘canGoBack’ can not be referenced from a Sendable closure.” One nuance depends on where the property was declared: WKWebView comes in from Objective-C with preconcurrency isolation, so the diagnostic lands as a warning in Swift 6 mode, while the identical shape against a Swift-declared @MainActor property is a hard error13. KVO delivers synchronously on the thread that mutated the value, and WKWebView navigation state mutates only on the main thread, so the read is sound. Assert it with MainActor.assumeIsolated, which removes the Task hop entirely and stays synchronous8:

let pushNav: @Sendable (WKWebView?) -> Void = { [weak self] webView in
    MainActor.assumeIsolated {
        guard let self else { return }
        // safe to read webView?.canGoBack synchronously
    }
}

assumeIsolated is a promise to the compiler, not a question. Use it only where the runtime invariant genuinely holds (a documented main-thread callback), because a wrong promise is a crash, not a warning.

6. Heavy work that should not be on main. This one the checker will not flag, and it is the most important to catch yourself. Under a main-actor default, a synchronous CPU-bound method (JSON decoding a large payload, resizing an image) runs on the main actor and janks your UI. The default keeps you on-main; @concurrent is how you leave on purpose4:

struct Report: nonisolated Codable {}

@concurrent
func decodeLargePayload(_ data: Data) async throws -> Report {
    try JSONDecoder().decode(Report.self, from: data)
}

Note the nonisolated on the conformance: the example does not compile without it. Under the module MainActor default the compiler now infers isolated conformances too (the InferIsolatedConformances upcoming feature, implied by -default-isolation MainActor and named explicitly in the approachable-concurrency set), so a plain struct Report: Codable {} gets a main-actor-isolated Decodable conformance and the decode line fails: “Main actor-isolated conformance of ‘Report’ to ‘Decodable’ cannot be used in @concurrent context”, tagged [#IsolatedConformances]12. The boundary appears the moment you use the escape hatch: @concurrent code runs away from the main actor, so every conformance it touches must be usable off it. SE-0470 gives the conformance-level spelling above; declaring the whole type nonisolated struct Report: Codable {} works too and opts out every member at once12. The June version of this post left the conformance implicit, and the example compiled then; on Xcode 27 the plain struct Report: Codable {} errors as written.

@concurrent offloads the function to the global executor and is mutually exclusive with @MainActor, a custom global actor, and nonisolated(nonsending), by design: a function either runs where its caller is or deliberately runs away from it, never ambiguously4. The discipline the new model asks for lives entirely in this pattern. Stay on main for everything that touches UI, and reach for @concurrent only for the work that measurably needs a background thread. Measuring got easier this cycle: the Swift executors instrument in Instruments 27 draws the Main Actor and the global executor as separate tracks, and Apple’s own session uses it to watch @concurrent move thumbnail rendering off the Main Actor.

What does nonisolated actually do?

Four of the boundaries above resolve with the same keyword, so it deserves a precise definition. nonisolated opts a declaration out of actor isolation. Under SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor it is the single opt-out the whole model rests on: a nonisolated member belongs to no actor, so any context can use it without an await or an actor hop2. The keyword wears four faces.

On a function or a property. The face that patterns one through three use: a pure method on a Sendable type, a singleton static, a shared constant. Each touches no actor state, nonisolated records that fact, and every call site stops erroring.

On a type or an extension. Since SE-0449 (Swift 6.1), nonisolated also applies to a whole type or extension and propagates to every member11:

nonisolated struct URLBuilder {
    let base: String
    func make(_ q: String) -> URL? { URL(string: base + q) }
}

Reach for it when a helper type has no business on the main actor; one keyword on the declaration beats annotating member by member. Apple treats the spelling as ordinary vocabulary now: the iOS 27 SwiftUI additions ship nonisolated func declarations throughout, from reorderContainer to fileExporter.

As nonisolated(nonsending). SE-0461’s explicit spelling for an async function that runs on whatever actor called it: the function stays nonisolated, but execution never leaves the caller’s context3. Approachable concurrency makes exactly that the default for nonisolated async functions, and @concurrent is the opposite pole. The compiler refuses a function that claims both: “has multiple actor-isolation attributes (@concurrent and ‘nonisolated(nonsending)’).”13 Remember the gating from earlier: bare Swift 6 mode, even on the 6.4 compiler, still defaults async functions to the global executor; the build setting is what flips them to caller-runs.

On a conformance. The newest face, and the one pattern six hits: under the main-actor default your conformances are main-actor-isolated too, and SE-0470’s struct Report: nonisolated Codable {} is how generic and @concurrent code gets to use them12.

One thing nonisolated is not: nonisolated(unsafe). Plain nonisolated is compiler-checked and safe; nonisolated(unsafe) is an unchecked promise that you guard the state yourself, a sibling of assumeIsolated, not of anything else on this page.

When the defaults are wrong for you

Main-actor-by-default fits apps: SwiftUI and UIKit code is overwhelmingly main-thread, and the default matches reality. It fits less well in two cases, and pretending otherwise wastes your time.

  • A library or framework target with no UI. A networking layer, a parser, or a data engine has no reason to default to the main actor, and doing so forces @concurrent or nonisolated on nearly everything. Leave SWIFT_DEFAULT_ACTOR_ISOLATION unset for those targets and isolate deliberately, the old way around.
  • An actor-heavy concurrent system. If your design genuinely runs many things in parallel (a real pipeline, not an app with a few background tasks), the main-actor default fights you. You want explicit actors and nonisolated code, and the SE-0466 default is the wrong starting point.

For an app, though, the call is easy: turn both settings on, let the error count collapse, and treat the handful that remain as a map of exactly where your code leaves the main thread. That map is worth having. The old model gave you a thousand warnings and no map; the new one gives you six honest boundaries and a default that finally matches how the app runs.

One last note on noise versus signal. SourceKit will show cross-file index errors (“Cannot find type X in scope,” “No such module”) inside the editor while Xcode rebuilds its index, especially right after regenerating a project. Those are index artifacts, not concurrency errors. If xcodebuild reports BUILD SUCCEEDED, the concurrency model is satisfied and the editor is just catching up1. Chasing index ghosts is the fastest way to waste an afternoon on a migration that already worked. When Xcode 27 genuinely stops compiling something, the errors look different from index noise, and the @State macro post walks the sharpest example. One concurrency fix also rode along in that release: code inside #Preview now runs explicitly on the main actor, so preview bodies call main-actor-isolated APIs without concurrency warnings or runtime check failures14.

FAQ

How do I enable main-actor-by-default concurrency in Swift 6.2?

Set two build settings, on by default for new projects since Xcode 26 and unchanged in Xcode 27: SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor and SWIFT_APPROACHABLE_CONCURRENCY = YES5. In a Swift package you set SwiftSetting.defaultIsolation(MainActor.self) plus the NonisolatedNonsendingByDefault upcoming feature through swiftSettings instead6.

What does SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor actually change?

It makes a module’s code main-actor-isolated by default (SE-0466), so a view, its model, and its helpers are all @MainActor with no annotations2. Concurrency becomes the named exception you reach for rather than the ambient condition you annotate against on every line.

What is @concurrent and when should I use it?

@concurrent offloads an async function to the global executor, on purpose, away from its caller’s actor4. Under a main-actor default you use it for the work that measurably needs a background thread (decoding a large payload, resizing an image); it is mutually exclusive with @MainActor so a function either runs where its caller is or deliberately runs away from it.

Why do I get “Main actor-isolated … can not be referenced from a nonisolated context”?

Under the main-actor default, pure functions, singleton statics, and constants inherit main-actor isolation, then error when called from a nonisolated context. At a default-argument site the current compiler words it as “Main actor-isolated default value in a nonisolated context”; a direct reference in a nonisolated function body keeps the older static-property wording. The fix is a precise nonisolated annotation on the member, not a suppression; the value is safe to share because it touches no actor state7.

Should I use main-actor-by-default for a library or framework target?

No. A networking layer, parser, or data engine has no reason to default to the main actor, and doing so forces @concurrent or nonisolated on nearly everything. Leave SWIFT_DEFAULT_ACTOR_ISOLATION unset for non-UI targets and isolate deliberately the old way around.

Does awaiting a nonisolated async function still hop actors in Swift 6.2?

No, with approachable concurrency enabled: SE-0461 makes a nonisolated async function run on whatever actor called it instead of hopping to the global executor, so awaiting it no longer drags you across an isolation boundary or demands Sendable on everything in scope3.

What does nonisolated mean in Swift?

nonisolated opts a declaration out of actor isolation, so any context can use it without an await or an actor hop. Under the Swift 6.2 model it applies to functions and properties (patterns one through three above), to whole types and extensions since SE-044911, to async functions as nonisolated(nonsending) per SE-04613, and to protocol conformances per SE-047012. Plain nonisolated is compiler-checked and safe; only nonisolated(unsafe) is an unchecked promise.

What is nonisolated(nonsending) in Swift 6.2?

nonisolated(nonsending) is SE-0461’s explicit spelling for an async function that runs on whatever actor called it instead of hopping to the global executor3. Approachable concurrency makes it the default for nonisolated async functions, @concurrent is the opposite mode, and the compiler rejects a function that declares both.



  1. Author’s production code across the 941-family iOS apps (Ki, Return, Get Bananas), all shipping with SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor and SWIFT_APPROACHABLE_CONCURRENCY = YES. The six error patterns and fixes below were the complete strict-concurrency cleanup set for Ki 1.0.0. ↩↩↩

  2. Swift Evolution, SE-0466: Control default actor isolation inference. Lets a module default to @MainActor isolation, so UI and app targets run on the main actor unless code opts out via @concurrent or an explicit actor. ↩↩↩↩

  3. Swift Evolution, SE-0461: Run nonisolated async functions on the caller’s actor by default. A nonisolated async function defaults to nonisolated(nonsending), running on the caller’s actor instead of hopping to the global executor, which removes the boundary-crossing Sendable requirements that came with the hop. ↩↩↩↩↩↩↩

  4. Swift Evolution, SE-0461 introduces the @concurrent attribute to opt a function into running on the global executor (a background thread). @concurrent and nonisolated(nonsending) are the two opposing isolation modes for a nonisolated async function: a function either runs where its caller is or deliberately runs away from it. @concurrent cannot be combined with @MainActor or a custom global actor. ↩↩↩↩

  5. SWIFT_APPROACHABLE_CONCURRENCY is the umbrella Xcode build setting for the approachable-concurrency feature set, and SWIFT_DEFAULT_ACTOR_ISOLATION selects the module’s default isolation. Xcode 27’s build-system specification (Swift.xcspec inside build 27A266a) maps the umbrella to exactly five upcoming features: DisableOutwardActorInference, GlobalActorIsolatedTypesUsability, InferIsolatedConformances, InferSendableFromCaptures, and NonisolatedNonsendingByDefault; the same bundle’s new-project templates set SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor in the App Base template and SWIFT_APPROACHABLE_CONCURRENCY = YES in the base project settings, exactly as Xcode 26 introduced them. Secondary walkthroughs: Donny Wals, “Exploring concurrency changes in Swift 6.2”, and Paul Hudson, “What’s new in Swift 6.2”. ↩↩↩↩

  6. Swift, Swift Concurrency Migration Guide (source repository: swiftlang/swift-migration-guide). In a Swift package, SE-0466 defines the dedicated SwiftSetting.defaultIsolation(MainActor.self) setting for the module default, paired with .enableUpcomingFeature("NonisolatedNonsendingByDefault") for the caller-runs behavior. ↩↩

  7. Swift, Migration Guide: global actor isolation and nonisolated. A @MainActor type’s methods inherit main-actor isolation; nonisolated opts a method out, which is correct for pure functions that touch no isolated state. ↩↩

  8. Apple Developer, MainActor.assumeIsolated(_:). Asserts that the current execution is already on the main actor and runs the closure synchronously without an actor hop. The assertion traps at runtime if the invariant does not hold, so it is valid only where the caller is guaranteed to be on the main thread. ↩

  9. Apple, WWDC 2026 Swift Group Lab. Remarks attributed to Holly and Doug of the Swift core team are paraphrased from a recording transcribed locally; Apple published no official transcript or captions for the session, so the wording is approximate, not a verbatim quote. ↩

  10. Swift compiler diagnostics, Actor-Isolated Call. Swift 6.4 appends a bracketed group tag such as [#ActorIsolatedCall] to its concurrency diagnostics, each linking to a page that explains the group and its fixes. ↩

  11. Swift Evolution, SE-0449: Allow nonisolated to prevent global actor inference, implemented in Swift 6.1. Extends nonisolated to types, extensions, and protocols, where it opts out every member at once. ↩↩

  12. Swift Evolution, SE-0470: Global-actor isolated conformances, implemented in Swift 6.2, and the compiler’s Isolated Conformances diagnostics page. Under the InferIsolatedConformances upcoming feature, which -default-isolation MainActor implies and SWIFT_APPROACHABLE_CONCURRENCY enables explicitly, a main-actor-isolated type’s conformances are main-actor-isolated as well; nonisolated on the conformance opts it out. ↩↩↩↩↩

  13. Author reproduction on Xcode 27.0 (27A266a), Apple Swift 6.4 (swiftlang-6.4.0.34.1), macOS 26.6.2, September 15, 2026, with swiftc -typecheck -swift-version 6 -default-isolation MainActor. Diagnostics as printed: pattern one, error: call to main actor-isolated instance method 'searchURL(for:)' in a synchronous nonisolated context [#ActorIsolatedCall]; patterns two and three at the default-argument site, error: main actor-isolated default value in a nonisolated context, and the body-reference form, error: main actor-isolated static property 'shared' can not be referenced from a nonisolated context; pattern four’s weak-self shape, in both a NotificationCenter observer and a pure @Sendable closure variant, compiled with no diagnostics, while a @Sendable closure over a local var mutated afterward produced error: reference to captured var 'count' in concurrently-executing code [#SendableClosureCaptures]; pattern five against the macOS 27 and iOS 27 SDKs’ WKWebView.canGoBack, warning: main actor-isolated property 'canGoBack' can not be referenced from a Sendable closure, and against a Swift-declared @MainActor property, the same text as an error; pattern six as published in June, error: main actor-isolated conformance of 'Report' to 'Decodable' cannot be used in @concurrent context [#IsolatedConformances], with both struct Report: nonisolated Codable {} and nonisolated struct Report: Codable {} compiling clean; attribute conflicts, error: global function 'bad1()' has multiple actor-isolation attributes (@MainActor and @concurrent) and the matching (@concurrent and 'nonisolated(nonsending)') form; hasFeature(NonisolatedNonsendingByDefault) reported off in bare -swift-version 6 mode, and hasFeature(InferIsolatedConformances) off in bare mode, on with -default-isolation MainActor. ↩↩↩↩↩

  14. Apple, Xcode 27 Release Notes, fetched September 15, 2026: “Fixed: Code inside #Preview now explicitly runs on the main actor, so it can call main-actor-isolated APIs without producing concurrency warnings or runtime check failures.” (177394870) ↩

Related Posts

What Apple's Swift Team Said in the WWDC26 Lab

Apple's Swift Group Lab at WWDC26 ran without captions. We transcribed it locally. Candid engineer answers on concurrenc…

14 min read

What's New in Swift (2026): The WWDC26 Update

Swift 6.3 and 6.4 from WWDC26: anyAppleOS availability, module selectors, borrow/mutate accessors, the Iterable protocol…

19 min read

Two MCP Servers Made Claude Code an iOS Build System

XcodeBuildMCP and Apple's Xcode MCP give Claude Code structured access to iOS builds, tests, and debugging. Setup, real-…

19 min read