Which accesses to an actor compile without await, which do not compile at all, and what do nonisolated members change?

10.9.1 · L3 · Interview

Predict first

Does this compile, and what does it print?

actor Account {
    var balance = 0
    let id = 7
    nonisolated let name = "acct"
    nonisolated var label: String { "acct-\(id)" }
}
let a = Account()
print(a.name, a.label)
  • It compiles and prints "acct acct-7"
  • Compile error: needs await
  • Compile error: nonisolated is not allowed on a let

Predict first

Does reading the mutable property from outside compile?

actor Account { var balance = 0 }
let a = Account()
print(a.balance)
  • Compile error
  • Compiles and prints 0

Predict first

And calling a method with no await?

actor Account { var balance = 0; func deposit(_ n: Int) { balance += n } }
let a = Account()
a.deposit(5)
  • Compile error
  • Compiles, runs synchronously

Mental model. The actor is a locked office. A visitor can ask the receptionist (an await) to fetch things, or read the nameplate on the door (nonisolated let), but cannot walk in and read the papers on the desk.

From outside an actor, you may call its methods and read its state only asynchronously. Reading a.balance from a different isolation is a compile error (“actor-isolated property can not be referenced from the main actor”), and calling a.deposit(5) synchronously is an error (“call to actor-isolated instance method in a synchronous main actor-isolated context”); the fix is await a.deposit(5) from an async context. Mutable stored properties are never reachable from outside, even for reading, without an await hop through an actor method.

nonisolated opts a member out of the actor’s isolation so it is callable synchronously from anywhere. It is allowed on an immutable let of a Sendable type (name) and on computed properties or methods that only touch such state (label reads the let id), which is why print(a.name, a.label) compiles with no await. It is not allowed to read mutable state. This is how an actor can satisfy synchronous protocol requirements such as CustomStringConvertible.description or Hashable: implement them nonisolated and use only immutable data (an id), never the mutable state.

Immutable Sendable lets and members built from them can be nonisolated and read synchronously.

Mutable actor state cannot be read from another isolation without an await.

Crossing isolation requires await in an async context.

Common traps

  • Marking a method nonisolated to silence the error. It then cannot touch the mutable state, and you have moved the problem, not solved it.
  • Expecting reads to be free. Even a read of a mutable property from outside is an asynchronous hop.

They might follow up with

  • Can an actor conform to Equatable? Yes, with a nonisolated == that compares immutable identity, such as an id.
  • Why does the compiler allow a.name synchronously? An immutable Sendable let can never change, so no isolation is needed.

Related questions

  • 10.9.2 What is an isolated parameter, and when would you write a function that takes one instead of making it an…
  • 10.9.3 How do you define your own global actor, what does annotating a type with it do, and when is it better than…
  • 10.9.4 Where does code actually run after an await: what do @MainActor, MainActor.run and a nonisolated async…
  • 10.4.1 What does an actor guarantee, and what is actor reentrancy?
  • 10.4.2 How do @MainActor, global actors, isolated and nonisolated work, and how does isolation propagate?

What is an isolated parameter, and when would you write a function… →