SwiftQA Random

What is nonisolated(nonsending)?

nonisolated(nonsending) is the other half of the pair alongside @concurrent — it’s the explicit spelling for “run on the caller’s actor” behavior, as opposed to @concurrent which forces the background executor.

Why it exists

Swift 6.2 changes how async functions pick a thread by default via SE-0461. Under the old rules, a plain nonisolated async func always hopped to the global executor — no way to say “actually, just stay wherever you were called from” without wrapping things in @MainActor or restructuring.

nonisolated(nonsending) is that missing option, spelled out explicitly:

nonisolated(nonsending)
func loadUserPreferences() async -> Preferences {
    // does NOT jump to the global executor —
    // stays on whatever actor called it
    ...
}
  • Called from @MainActor code → this runs on the main actor.
  • Called from another actor → it stays on that actor.
  • Called from truly non-isolated code → runs non-isolated, no actor.

The name describes the mechanism: the function doesn’t “send” its execution off to a different isolation domain — no crossing of an actor boundary, so no Sendable requirements are triggered on the way in or out for that hop.

Why “nonsending” and not just “runs wherever”

When you cross an actor boundary (i.e. do get sent elsewhere), Swift’s data-race checker requires whatever you pass across that boundary to be Sendable. Because nonisolated(nonsending) functions never cross a boundary — they inherit the caller’s isolation — the arguments/results don’t need to satisfy Sendable for that call. This is a big part of why it reduces the wall of Sendable errors people hit under strict concurrency: you’re not actually parallelizing, so the compiler doesn’t need those guarantees.

How it relates to the new default

In Swift 6.2 with the approachable-concurrency feature enabled, nonisolated(nonsending) effectively becomes the default for nonisolated async func — Swift 6.2 inverts concurrency: default your module to MainActor (SE-0466), run nonisolated async on the caller (SE-0461), escape with @concurrent. So in practice you’d write it explicitly mainly for clarity, or when your module hasn’t opted into the new default mode yet but you want this one function to behave the new way.

Side-by-side with @concurrent:

AttributeWhere it runsCrosses actor boundary?Sendable required?
nonisolated(nonsending)caller’s actor (or none)NoNo
@concurrentglobal executor, alwaysYes (unless caller was already non-isolated)Yes

They’re mutually exclusive opposites on a nonisolated function — you pick one to make your intent explicit rather than relying on old implicit background-hopping behavior. And to reiterate the caveat from before: this all depends on your project having the Swift 6.2 approachable-concurrency upcoming feature enabled, since it’s not the automatic behavior for existing Swift 6.0/6.1 code.