SwiftQA Random

What does @concurrent do?

@concurrent is a Swift 6.2 attribute that does the opposite of what you might guess — it forces a function to run on the background/global executor rather than the caller’s actor.

The context you need first: Swift 6.2 flipped the default

Before 6.2, a nonisolated async function always hopped to the global executor (background thread) when called, regardless of the caller’s context. That was a constant source of surprise — you’d call a plain async function from @MainActor code and suddenly be off the main actor with Sendable errors everywhere.

Swift 6.2, via SE-0461, changes how async functions pick a thread by default — an unmarked nonisolated async function now inherits the caller’s actor by default, so an await from a @MainActor view model keeps running on the main thread unless you say otherwise. This pairs with SE-0466, which lets you make @MainActor the default isolation for a whole module — the “approachable concurrency” push.

So what does @concurrent actually do?

Marking a function @concurrent means it always runs on the global executor (background thread) — it’s your explicit opt-out from the new “stay on the caller’s actor” default:

@concurrent
func decode<T: Decodable>(_ data: Data) async throws -> T {
    let decoder = JSONDecoder()
    // runs on a background thread, regardless of caller
    return try decoder.decode(T.self, from: data)
}

When you have genuinely CPU-intensive work — decoding a large JSON blob, processing an image, crunching numbers — you mark the function @concurrent, telling Swift “run this off the caller’s actor.”

A few important rules:

  • @concurrent cannot be combined with @MainActor or a custom global actor — it only applies to nonisolated functions, since the whole point is escaping actor isolation.
  • @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.
  • The design philosophy is deliberately conservative: the smallest unit of work possible is made @concurrent to avoid introducing loads of concurrency where it isn’t needed, since concurrency comes with a real complexity cost.

Mental model comparison:

Pre-6.2 default6.2+ default (nonisolated(nonsending))@concurrent
nonisolated async func runs onglobal executor (background)caller’s actorglobal executor (background), always

So in effect, @concurrent is the new spelling of “what nonisolated async used to mean automatically” — except now it’s opt-in and explicit, rather than a silent default you had to work around.

One practical note: this all requires enabling the Swift 6.2 upcoming feature flag (SWIFT_APPROACHABLE_CONCURRENCY / AsyncCallerExecution, or setting defaultIsolation in your package manifest) — it’s not automatically retroactive to existing 6.0/6.1 code without opting in.