---
title: "Swift Actors: Isolation and Concurrency Safety"
slug: "swift-actors"
category: "coding"
tags: ["swift", "concurrency", "actors", "threading", "swift6"]
status: "stable"
last_updated: 2026-05-14
summary: "How to use Swift actors, @MainActor, nonisolated, and Sendable to eliminate data races at compile time."
related: ["[[coding/swift]]", "[[coding/swift-async-await]]", "[[coding/swift-value-types]]", "[[ios/swiftui]]", "[[ios/core-data]]"]
---

## Overview

Swift 6 enforces actor isolation at compile time. Every mutable stored property must belong to exactly one isolation domain, and crossing that boundary requires `await`. This page covers how to define actors, annotate types with `@MainActor`, opt out with `nonisolated`, and satisfy the `Sendable` checker. Read [[coding/swift]] first; the rules here extend the concurrency section.

## Define an actor when mutable state needs shared access

Use `actor` instead of `class` whenever multiple concurrent callers read or write the same mutable state. The compiler serializes access automatically.

```swift
actor TokenBucket {
    private var tokens: Int
    private let max: Int

    init(max: Int) {
        self.max = max
        self.tokens = max
    }

    func consume() -> Bool {
        guard tokens > 0 else { return false }
        tokens -= 1
        return true
    }

    func refill() {
        tokens = max
    }
}

// Caller must await because access crosses actor boundary
let allowed = await bucket.consume()
```

An actor's stored properties are only accessible from within its isolation domain without `await`. The compiler rejects synchronous cross-actor access.

## Use `@MainActor` for all UI-bound state

Mark every type that reads or writes UIKit or SwiftUI state with `@MainActor`. This ensures updates run on the main thread and the compiler enforces it.

```swift
@MainActor
final class ProfileViewModel: ObservableObject {
    @Published var displayName: String = ""
    @Published var isLoading = false

    func refresh() async {
        isLoading = true
        defer { isLoading = false }
        displayName = try? await fetchDisplayName() ?? displayName
    }
}
```

`@MainActor` on a type propagates to all stored properties and methods. You do not need to annotate each member individually. For [[ios/swiftui]] views, `@MainActor` is already inferred; you only need to annotate the view model.

## Mark pure methods `nonisolated` to avoid unnecessary hops

A method that reads only immutable state does not need actor isolation. Mark it `nonisolated` so callers can invoke it synchronously without crossing the isolation boundary.

```swift
actor Config {
    private var overrides: [String: String] = [:]
    let environment: String  // immutable; safe to expose nonisolated

    init(environment: String) {
        self.environment = environment
    }

    nonisolated func describe() -> String {
        "Config(\(environment))"
    }
}
```

Do not mark mutating methods `nonisolated`. The compiler rejects any attempt to mutate actor-isolated state from a `nonisolated` context.

## Conform to `Sendable` explicitly for types that cross boundaries

A value or reference that moves between actors must conform to `Sendable`. Structs with all-`Sendable` stored properties conform automatically. Classes need either immutability or a manual conformance with a justification comment.

```swift
// Automatic: all stored properties are Sendable
struct WorkItem: Sendable {
    let id: UUID
    let payload: Data
}

// Manual: requires a lock inside; justify why it is safe
final class Cache<Key: Sendable, Value: Sendable>: @unchecked Sendable {
    private let lock = NSLock()
    private var store: [Key: Value] = [:]

    func set(_ value: Value, for key: Key) {
        lock.withLock { store[key] = value }
    }
}
```

`@unchecked Sendable` silences the compiler without removing the obligation. Add a comment that explains the locking strategy. See [[coding/swift-value-types]] for why structs are preferred and usually cheaper.

## Avoid `nonisolated(unsafe)` in Swift 6 code

`nonisolated(unsafe)` opts a stored property out of isolation checking. It is the successor to `@_unsafeInheritExecutor` and equally dangerous: it moves the race-free obligation to the programmer. Reserve it for bridging with C or Objective-C APIs that have no Swift 6 annotations, and comment exactly why it is safe.

## Cross actor boundaries at the outermost call site

Push `await` to the boundary between subsystems, not deep inside implementation code. An actor method that calls another actor creates a suspension chain; keep that chain as shallow as possible.

```swift
// Prefer: one await at the integration layer
func handlePurchase(item: PurchaseItem) async throws {
    let receipt = await store.record(item)    // actor call
    await analytics.track(.purchase(receipt)) // actor call
    await ui.confirmPurchase(receipt)         // MainActor call
}
```

Flat chains are easier to reason about and easier to test. A method that awaits several actors in sequence is a natural integration point for a test double.

## Prefer actors over serial dispatch queues

`DispatchQueue(label:)` with a serial queue is the Objective-C solution to the same problem actors solve. New Swift 6 code should use actors. The compiler verifies actor isolation; it cannot verify queue discipline. Existing queue-based code should be wrapped in an actor during migration so the isolation is explicit.

## Related

- [[coding/swift]]
- [[coding/swift-async-await]]
- [[coding/swift-value-types]]
- [[coding/swift-protocols]]
- [[ios/swiftui]]
- [[ios/core-data]]
