Actor isolation keywords in Swift 6.2 — nonisolated, isolated, @concurrent, @globalActor, any Actor

Swift Concurrency has a handful of keywords that control where code runs: nonisolatedisolated@concurrent@globalActor. They all answer the same question — which executor does this function run on? — just in different ways.

nonisolated — run on the caller's executor

When you mark a function nonisolated inside an actor, it doesn’t hop off to some generic executor. With Approachable Concurrency enabled (default from Swift 6.2, opt-in via SWIFT_UPCOMING_FEATURE_APPROACHABLE_CONCURRENCY = YES in earlier versions), a nonisolated function runs on the caller’s executor instead.

This was the original Swift Concurrency behaviour. SE-0338 changed it so that nonisolated async functions always ran off-actor. That change was later reverted in Swift 6.2 — so we’re back to running on the caller’s executor. nonisolated is now equivalent to nonisolated(nonsending).
				
					actor MyActor {
  var value = "abc"

  func test() async {
    await nonisolatedFuncCalledFromSelfActor()

    Task { @MainActor in
      await nonisolatedFuncCalledFromMainActor()
    }
  }

  nonisolated func nonisolatedFuncCalledFromSelfActor() async {
    self.assertIsolated() // ✓ — runs on MyActor's executor
    // self.value = "x"  ← ✗ compiler still sees this as nonisolated
  }

  nonisolated func nonisolatedFuncCalledFromMainActor() async {
    MainActor.assertIsolated() // ✓ — caller was @MainActor
  }
}
				
			

Notice the catch: even though assertIsolated() passes, the compiler still treats the function as nonisolated at the type-system level, so you can’t touch actor-isolated properties. The runtime and the type system are telling you different things — nonisolated describes the static view, not what’s actually happening on the thread.

Why is this useful? Less hopping on and off executors. Every unnecessary hop is overhead — the function suspends, waits for the target executor, and resumes. If the caller is already on the right executor, nonisolated avoids all of that.

@concurrent — force a hop off the actor

Sometimes you want the opposite — explicitly force the function off the actor and onto the global concurrent executor, regardless of who called it. That’s what @concurrent is for.

				
					actor MyActor {
  // Always hops to global concurrent executor
  @concurrent nonisolated func concurrentNonisolatedFuncCalledFromSelfActor() async {
    // self.assertIsolated() ← ✗ not on MyActor's executor
  }

  // @concurrent automatically marks the func as nonisolated too
  @concurrent func concurrentFuncCalledFromSelfActor() async {
    // self.assertIsolated() ← ✗ not on MyActor's executor
  }
}
				
			

isolated parameter — borrow another actor's isolation

Putting isolated before a parameter transfers the function’s isolation to that actor. The function is no longer isolated to self — it runs on the actor passed as an argument.

				
					actor ActorA {
  var x = "abc"

  func test() async {
    let b = ActorB()

    // Must await — needs to hop to b's executor
    await changeOtherActorStateWithIsolationToOtherActor(b)

    // No await — already on self's executor
    changeOtherActorStateWithSelfActorIsolation(b)
  }

  // Isolated to b, NOT to self
  func changeOtherActorStateWithIsolationToOtherActor(_ b: isolated ActorB) {
    b.y = "new"        // ✓
    // self.x = "new"  ← ✗ no longer isolated to self
  }

  // Isolated to self, b is just a regular parameter
  func changeOtherActorStateWithSelfActorIsolation(_ b: ActorB) {
    x = "new"          // ✓
  }
}
				
			

Only one parameter per function can be marked isolated.

isolated (any Actor) and #isolation

You can take it further and write functions that work with any actor dynamically. And with #isolation, the caller’s actor is captured automatically at the call site — no need to pass it explicitly.

				
					// #isolation captures the caller's actor at the call site
func changeIsolatedActorVariableWithDefaultIsolatedAny(
  isolation: isolated (any Actor) = #isolation
) {
  if let a = isolation as? MyActor {
    // Compiler doesn't know the dynamic type, so we assert it
    a.assumeIsolated { isolated in
      isolated.value = "abc" // ✓
    }
  }
}

actor Caller {
  func go() {
    changeIsolatedActorVariableWithDefaultIsolatedAny() // isolation = self, no await
  }
}
				
			

#isolation is a compiler macro — it captures the surrounding isolation at the call site, not at the function definition.

@globalActor — a shared singleton actor

@MainActor is the most familiar example of a global actor, but you can define your own. A global actor is a singleton actor that multiple types can all be isolated to — since they share the same executor, they can access each other’s state directly without awaiting.

				
					// Defining a new global actor
@globalActor
actor DatabaseActor {
  static let shared = DatabaseActor()
}

// Both classes share the same actor — they can access each other freely
@DatabaseActor class Repository {
  var cache: [String] = []
}

@DatabaseActor class QueryService {
  func invalidate(_ repo: Repository) {
    repo.cache = [] // ✓ same actor, no await needed
  }
}

// From outside — must hop in explicitly
Task.detached {
  // repo.cache = []  ← ✗ not isolated to DatabaseActor
  Task { @DatabaseActor in
    repo.cache = [] // ✓
  }
}