A Practical Guide to nonisolated in Swift Concurrency

Swift’s concurrency model leans heavily on isolation—especially actor isolation—to prevent data races. The nonisolated modifier is a precision tool that lets you selectively opt out of that isolation for specific members, so you can call them without hopping to the actor’s executor (and thus without await). Used well, it helps you keep APIs fast and ergonomic while preserving safety.


What nonisolated Means

Definition: nonisolated marks an actor (or global‐actor–isolated type) member as not isolated to that actor. Such members:

Where it applies:

Tip: nonisolated does not make code magically thread-safe. It only says, “this member doesn’t require actor isolation,” so you must design it to truly not depend on isolated state.


How It Interacts with Actor Isolation

By default, every instance member of an actor is actor-isolated. Calling it from outside the actor requires await, which hops to the actor’s executor.

Marking a member nonisolated removes that isolation—no hop, no suspension. But that freedom comes with a rule: the member cannot access actor-isolated state. The compiler enforces this for safe nonisolated (see nonisolated(unsafe) caveat later).

actor Counter {
    private var value = 0

    func increment() {            // isolated to Counter
        value += 1
    }

    nonisolated func version() -> String {  // no hop, no await
        "1.0"
    }

    nonisolated var isBeta: Bool {          // computed, no actor state
        false
    }
}

Calling site:

let counter = Counter()
await counter.increment()  // requires hop

counter.version()          // no await ✅
counter.isBeta             // no await ✅

If a nonisolated member touches isolated state, the compiler flags it:

actor Counter {
    private var value = 0

    nonisolated func snapshot() -> Int {
        value            // ❌ error: actor-isolated property 'value' can’t be accessed from a nonisolated context
    }
}

Use Cases That Fit nonisolated

  1. Pure utilities and constants Methods that compute results from inputs only (or constants), not from actor state.

    actor UUIDProvider {
        nonisolated static let namespace = UUID(uuidString: "6ba7b810-9dad-11d1-80b4-00c04fd430c8")!
    
        nonisolated func make() -> UUID { UUID() } // safe: no actor state
    }
    
  2. Protocol conformances that should be usable everywhere Many protocols are most ergonomic when conforming members are callable without await:

    • CustomStringConvertible.description
    • Hashable.hash(into:)
    • Equatable.==
    • Comparable.<
    • Error, LocalizedError, etc.
    actor User: CustomStringConvertible, Hashable {
        let id: UUID
        private var name: String
    
        init(id: UUID, name: String) {
            self.id = id
            self.name = name
        }
    
        nonisolated var description: String { "User(\(id))" } // uses only 'id' (immutable)
    
        nonisolated func hash(into hasher: inout Hasher) {
            hasher.combine(id)  // avoid isolated state like 'name'
        }
    
        nonisolated static func == (lhs: User, rhs: User) -> Bool {
            lhs.id == rhs.id
        }
    }
    
  3. Global-actor types that need “fast path” members On @MainActor types (view models, controllers), you might want some members callable off the main thread without hopping:

    @MainActor
    final class SessionViewModel {
        let sessionID = UUID()
    
        nonisolated var stableID: UUID { sessionID } // ❌ usually illegal: touches isolated state
        // Fix: capture an immutable copy during init into a nonisolated-friendly store.
    }
    
    @MainActor
    final class SafeSessionViewModel {
        private let _id: UUID  // initialized once; never mutated
        init() { _id = UUID() }
    
        nonisolated var stableID: UUID { _id }  // ✅ not accessing main-actor state; it's just a private, immutable value
    }
    
  4. Logging / metrics / feature flags that don’t depend on actor state. Keep hot-path logging synchronous and hop-free.


Correct vs. Incorrect Usage

Correct

actor Math {
    nonisolated func fib(_ n: Int) -> Int {
        // Pure function: no actor state
        if n < 2 { return n }
        var a = 0, b = 1
        for _ in 2...n { (a, b) = (b, a + b) }
        return b
    }
}

Incorrect (accessing isolated state)

actor Cache {
    private var storage: [String: String] = [:]

    nonisolated var count: Int {
        storage.count           // ❌ error: touches actor-isolated 'storage'
    }
}

Incorrect (derived from mutable isolated state—even if you could read it)

Even if you somehow bypass checks, exposing values derived from mutable isolated state as nonisolated is a data-race risk.

actor Clock {
    private var skewMillis: Int = 0

    nonisolated func now() -> Date {
        // ❌ Conceptually wrong: result depends on mutable actor state.
        Date().addingTimeInterval(TimeInterval(skewMillis) / 1000)
    }
}

nonisolated (safe)

nonisolated(unsafe) (escape hatch)

actor ImageCache {
    private var count = 0

    nonisolated(unsafe) func debugCount() -> Int {
        count // ⚠️ allowed, but racy; don’t ship this in production
    }
}

isolated (parameter modifier)

func dumpState(of actor: isolated Counter) {
    // Inside here, you’re on actor’s executor; can touch isolated state.
    // No 'await' needed for actor operations.
}

let c = Counter()
await dumpState(of: c)  // Call site must ensure isolation (await or be already on it).

Regular actor members (default)


Common Pitfalls

  1. Reading isolated state (directly or indirectly)

    • Direct field access is rejected for safe nonisolated.
    • Indirect reads (e.g., calling an isolated helper) also violate isolation.
  2. Leaking mutable state through “stable” views

    • Don’t expose references/pointers derived from isolated state; even if they look read-only, the underlying data may mutate concurrently.
  3. Forgetting global-actor context

    • @MainActor types are also “actor-isolated” to the main actor. nonisolated on those members still must avoid main-actor state.
  4. Overusing nonisolated(unsafe)

    • It compiles but undermines safety guarantees. Treat it like unsafeBitCast: last resort.
  5. Stored properties

    • nonisolated applies to methods / computed properties / subscripts. Stored properties remain isolated; design around that (capture immutable copies in init, or expose safe computed views).

Best Practices


Performance Considerations


Additional Examples

Protocol requirement declared nonisolated

You can declare a protocol API to be nonisolated so conforming actors provide hop-free implementations:

protocol Identity {
    nonisolated var id: UUID { get }
}

actor Account: Identity {
    private let _id = UUID()

    nonisolated var id: UUID { _id } // ✅ based on immutable state
}

Nonisolated subscript

actor Words {
    private let dictionary: Set<String>

    init(_ words: [String]) { dictionary = Set(words) }

    nonisolated subscript(candidate: String) -> Bool {
        candidate.allSatisfy(\.isLetter) // ✅ no actor state
    }
}

Mixing nonisolated with async members

actor Repository {
    private var items: [String] = []

    func add(_ s: String) { items.append(s) }   // isolated
    func all() -> [String] { items }            // isolated

    nonisolated func isValidKey(_ s: String) -> Bool { // pure validation
        !s.isEmpty && s.allSatisfy(\.isLetter)
    }
}

Summary

Used thoughtfully, nonisolated gives you the best of both worlds: Swift’s strong isolation guarantees and crisp, zero-hop APIs where they count.