Actors In Swift

Jan 17, 2026 Swift

Actor in Swift

Apple announced Actor in WWDC 2021 that shipped with Swift 5.5. WWDC

Developers used to often run into Data Races, and it would be more common than you think. Actors bring the most needed solution to this problem by means of isolation and async/await. Let’s unpack how this works in practice.

Actor Declaration and Isolation

You declare an actor with the actor keyword, which defines a special reference type. For example:

actor BankAccount {
    private(set) var balance: Double = 0.0

    func deposit(_ amount: Double) {
        balance += amount
    }

    func withdraw(_ amount: Double) {
        balance -= amount
    }
}

This BankAccount behaves like a class with a stored property balance and two methods. The key difference is that balance (and the methods above) are actor-isolated. They can only be accessed from within that specific BankAccount instance’s own execution context. If you try to read or write balance from outside the actor, the compiler will reject that. 1

Let’s see an example of that:

let account = BankAccount()
let current = account.balance

Attempting something like this results in an error:

actor-isolated property ‘balance’ can only be referenced from inside the actor

This happens because Swift enforces actor isolation. By default, all stored properties, computed properties, subscripts, and synchronous instance methods are isolated to self. In the example BankAccount, balance and the methods deposit(_:) and withdraw(_:) can only run inside the actor. 1

Behind the scenes, Swift transforms an actor into a class conforming to a hidden Actor protocol (which itself inherits Sendable). All actor types implicitly conform to Sendable because the actor guarantees serial access to its mutable state. In practice this means you can pass actor references across threads safely; the compiler will enforce that any interaction with an actor’s state is done through the actor’s isolated context. 1 2

Calling Actor Methods

Since actor-isolated state can’t be directly accessed from arbitrary threads, Swift requires that calls from outside the actor be made asynchronously. In concrete terms, any method you want to call from the outside of the actor must be marked async or return through a concurrency primitive. For example, to use the BankAccount actor, you do this:

let account = BankAccount()
Task {
    // Call deposit asynchronously - this enqueues a message on the actor
    await account.deposit(100.0)
    // After the deposit completes, we could read the balance (if we had an async getter)
}

Here await account.deposit(100.0) does not execute deposit(_:) immediately on the current thread. Instead, Swift enqueues a task (a message) to the actor’s private queue. The actor’s executor will pick up tasks from that queue one by one and run them to completion. As Gregor’s proposal notes, “an actor processes the messages in its mailbox sequentially, so that a given actor will never have two concurrently-executing tasks running actor-isolated code. This ensures that there are no data races on actor-isolated mutable state.” 1 2 3

Even reading a simple property requires await unless that property is actor-independent. For instance, we might add an async getter to BankAccount:

actor BankAccount {
    private(set) var balance: Double = 0.0

    func deposit(_ amount: Double) {
        balance += amount
    }

    func getBalance() async -> Double {
        return balance
    }
}

Task {
    await account.deposit(50.0)
    let b = await account.getBalance() // Must await to read balance
}

Because balance is var (mutable), the getBalance() method must be async to be called from outside. If we had tried to define func getBalance() -> Double (synchronous), the compiler would forbid calling it from outside the actor. In fact, Swift enforces that synchronous instance methods of an actor are actor-isolated and cannot be invoked from outside the actor. They cannot be placed on the queue, so they are effectively private to the actor’s context. Thus, any method intended for external use is typically marked async, and you always use await to call it.

Internally (within the actor), you can call its own methods directly without await because you are already on the actor’s executor. For example, inside another async method of the same actor, calling deposit(…) can just be a normal call. Outside the actor, every interaction becomes an asynchronous message.

Execution Model: An actor’s executor is like a private, serial dispatch queue. When you do await account.deposit(…), Swift transforms that into enqueuing a partial task on account’s queue. The actor will process tasks in FIFO order (unless other scheduling intervenes), always running one at a time. The runtime guarantees that “an actor never is concurrently running on multiple threads”. In practice, this means your actor code cannot have two threads inside it at the same time, eliminating classic data races on its isolated state. 1 2

Actor Reentrancy and Interleaving

A subtle but crucial feature of Swift actors is reentrancy. By default, Swift actors allow a form of controlled concurrency: if an actor-isolated async method suspends, the actor is free to start executing another message before the first method has fully returned. In other words, an actor can “re-enter” itself while a previous call is awaiting. As the Swift proposal explains, “Actor-isolated functions are reentrant. When an actor-isolated function suspends, reentrancy allows other work to execute on the actor before the original actor-isolated function resumes.” The result is interleaving: parts of one async call may run, then pause, then another async call to the same actor may run in the middle, and then the first one resumes. 1

You can find more about Actor Reentrancy here.

Avoiding Surprises

Given actor reentrancy, a good rule is to design methods so that state checks and updates happen atomically, without interruption. For example, instead of writing:

func withdraw(amount: Double) async throws {
    if balance < amount { 
        throw InsufficientFunds()
    }
    await Task.sleep(1)      // imaginary delay
    balance -= amount
}

which could allow balance to be modified by another task during the await, it’s often safer to perform the entire operation without suspension. You might restructure it as:

func withdraw(amount: Double) throws {
    if balance < amount { throw InsufficientFunds() }
    balance -= amount
}

and call that method with await from outside, knowing it won’t suspend in the middle. In fact, one recommendation is to put code that must remain atomic into a single synchronous actor-isolated method. If you truly need to do multiple awaits, re-check your preconditions after each await, or consider serializing calls another way. 1

Because Swift doesn’t currently offer a nonReentrant attribute, one pattern is to manually queue work inside the actor. For example, you could use an AsyncStream or a semaphore to process calls one by one, but this is advanced. In most cases, thoughtful API design – minimizing awaited sections or re-checking state – suffices. 1

Sendable and Actor Safety

A key consequence of actor isolation is that actors are implicitly thread-safe. The Swift type system marks all actor types as conforming to Sendable. This reflects the guarantee that no two threads can access an actor’s mutable state simultaneously; any cross-thread access is mediated by the actor’s queue. In code terms, you can freely move an actor reference between tasks or threads without special locks, because the compiler enforces that any interaction is done via async/await calls.

More concretely, consider passing an actor into a Task closure or as a completion handler argument. Since the actor ensures serialized access, there’s no data race even if multiple tasks attempt to call it concurrently – they simply queue up. Swift’s concurrency model thus “guarantee[s] that all access to [an actor’s] mutable state is performed sequentially”, which is why actors are considered Sendable by default.

Actor Independence and Nonisolated Members

By default, only the actor’s own isolated context can touch its mutable vars or synchronous methods. However, Swift provides ways to work with data that doesn’t need such strict isolation. We already saw that immutable let properties of value types are actor-independent. You can also annotate specific methods or properties as nonisolated if they are safe to call from any thread. For example, a static helper method or a computed property that only reads external state could be marked nonisolated, bypassing the actor checks. But use this sparingly: marking things nonisolated is like opting out of the actor’s protections, so you should only do it when you’re sure the code is inherently thread-safe.

Putting It All Together: An Example

Suppose we want an actor to manage an integer counter. Here’s a sample design:

actor Counter {
    private var value = 0

    func increment() {
        value += 1
    }

    func getValue() async -> Int {
        return value
    }
}

Because increment() is synchronous, it cannot be called from outside the actor. Instead, in real usage we’d mark it async too (or rely on the fact that calling it with await still queues it). A more typical usage might be:

actor Counter {
    private var value = 0

    func increment() async {
        value += 1
    }

    func getValue() async -> Int {
        return value
    }
}

let counter = Counter()
Task {
    for _ in 1...100 {
        await counter.increment()
    }
    print("Final count:", await counter.getValue())
}

Each await counter.increment() enqueues an increment on counter. The actor processes these one at a time, so even though we launched many tasks possibly in parallel, the actor serializes the increments. We never see a data race on value.

One subtlety: if increment() did some work and awaited inside (for example, awaiting a network call before actually incrementing), other tasks could interleave. In that case, we’d ensure correctness by either moving the increment before any awaits or re-checking state after any await. But as written, the increment is straightforward and safe.

Best Practices and Pitfalls

Closing Thoughts

By following the patterns above (encapsulating state changes, checking state after awaits, and using async for external calls), you can build concurrent Swift programs that are both efficient and easy to understand. Actors are a powerful feature, and a deep understanding of their isolation and reentrancy makes the difference between subtle bugs and rock-solid async code.

Sources swift.org, swiftrocks.com, hackmd.io 1 2 3 4 5