SE-313 introduced the nonisolated and isolated keywords as part of adding actor isolation control. Actors are a new way of providing synchronization for shared mutable states with the new concurrency framework.
If you’re new to actors in Swift, I encourage you to read my article Actors in Swift: how to use and prevent data races describing them in detail. This article will explain how to control method and parameter isolation when working with actors in Swift.
Understanding the default behavior of actors
By default, each method of an actor becomes isolated, which means you’ll have to be in the context of an actor already or use await to wait for approved access to actor contained data.
It’s typical to run into errors with actors like the ones below:
- Actor-isolated property ‘balance’ can not be referenced from a non-isolated context
- Expression is ‘async’ but is not marked with ‘await’
Both errors have the same root cause: actors isolate access to its properties to ensure mutually exclusive access.
Take the following bank account actor example:
actor BankAccount {
enum BankError: Error {
case insufficientFunds
}
var balance: Double
init(initialDeposit: Double) {
self.balance = initialDeposit
}
func withdraw(amount: Double) throws {
guard balance >= amount else {
throw BankError.insufficientFunds
}
balance -= amount
}
func deposit(amount: Double) {
balance = balance + amount
}
}
Actor methods are isolated by default but not explicitly marked as so. You could compare this to methods that are internal by default but not marked with an internal
keyword. Under the hood, the code looks as follows:
isolated func withdraw(amount: Double) throws {
guard balance >= amount else {
throw BankError.insufficientFunds
}
balance -= amount
}
isolated func deposit(amount: Double) {
balance = balance + amount
}
Though, marking methods explicitly with the isolated keyword like this example will result in the following error:
‘isolated’ may only be used on ‘parameter’ declarations
We can only use the isolated keyword with parameter declarations. However, as mentioned, the default for actor methods is isolated
, so there’s no need to use this keyword explicitly.
The Essential Swift Concurrency Course for a Seamless Swift 6 Migration.
Learn all about Swift Concurrency in my flagship course offering 57+ lessons, videos, code examples, and an official certificate of completion.
Marking actor parameters as isolated
Using the isolated
keyword for parameters can be pretty useful for preventing unnecessary suspensions points. In other words, you’ll need fewer await
statements for the same result.
To illustrate this, we could introduce a new Charger
type that charges money from a bank account:
struct Charger {
static func charge(amount: Double, from bankAccount: BankAccount) async throws -> Double {
try await bankAccount.withdraw(amount: amount)
let newBalance = await bankAccount.balance
return newBalance
}
}
As you can see, we have two different suspension points:
- One to withdraw the given amount
- Another one to read the new balance
We could optimize this in several ways, one including to add a new method to BankAccount
. However, sometimes you’re not in control of the actor internals. In this case, we can use the isolated
keyword in front of the parameter:
/// Due to using the `isolated` keyword, we only need to await at the caller side.
static func charge(amount: Double, from bankAccount: isolated BankAccount) async throws -> Double {
try bankAccount.withdraw(amount: amount)
let newBalance = bankAccount.balance
return newBalance
}
By using the isolated
parameter, we basically instruct the whole method to be isolated to the given actor. In this case, we instruct the whole method to be isolated to the BankAccount
actor. Since there can only be one isolation at the same time, you can only use one isolated
parameter. Obviously, you can also only use the isolated
keyword with an Actor
type.
Isolated Closure Parameters
Isolated parameters can be used inside closures as well. This can be useful if you want to perform constant operations around a given dynamic action. A common example is a database transaction:
actor Database {
func beginTransaction() {
// ...
}
func commitTransaction() {
// ...
}
func rollbackTransaction() {
// ...
}
/// By using an isolated `Database` parameter inside the closure, we can access `Database`-actor isolation from anywhere
/// allowing us to perform multiple database queries with just one `await`.
func transaction<Result>(_ transaction: @Sendable (_ database: isolated Database) throws -> Result) throws -> Result {
do {
beginTransaction()
let result = try transaction(self)
commitTransaction()
return result
} catch {
rollbackTransaction()
throw error
}
}
}
The transaction
closure contains an isolated parameter using the Database
actor. This results in a way for us to perform multiple database queries from outside the Database
while only having to await once:
let database = Database()
try await database.transaction { database in
database.insert("<some entity>")
database.insert("<some entity>")
database.insert("<some entity>")
}
Adding a generic isolated closure for any actor
We can take this one step further by adding an extension for any Actor
type:
extension Actor {
/// Adds a general `perform` method for any actor to access its isolation domain to perform
/// multiple operations in one go using the closure.
@discardableResult
func performInIsolation<T: Sendable>(_ block: @Sendable (_ actor: isolated Self) throws -> T) async rethrows -> T {
try block(self)
}
}
This would allow us to access the isolation domain of any actor and perform multiple operations on it in one go. For example, we could rewrite the earlier bank charging without the extra Charger
instance method:
let bankAccount = BankAccount(initialDeposit: 200)
try await bankAccount.performInIsolation { bankAccount in
try bankAccount.withdraw(amount: 20)
print("New balance is \(bankAccount.balance)")
}
Using the nonisolated keyword in actors
Marking methods or properties as nonisolated can be used to opt-out to the default isolation of actors. Opting out can be helpful in cases of accessing immutable values or when conforming to protocol requirements.
In the following example, we’ve added an account holder name to the actor:
actor BankAccount {
let accountHolder: String
// ...
}
The account holder is an immutable let and is therefore safe to access from a non-isolated environment. The compiler is smart enough to recognize this state, so there’s no need to mark this parameter as nonisolated explicitly.
However, if we introduce a computed property accessing an immutable property, we have to help the compiler a bit. Let’s take a look at the following example:
actor BankAccount {
let accountHolder: String
var details: String {
"Account holder: \(accountHolder)"
}
// ...
}
If we were to print out details right now, we would run into the following error:

The accountHolder
property is immutable, so we can explicitly mark the computed property as nonisolated and solve the error:
actor BankAccount {
let accountHolder: String
nonisolated var details: String {
"Account holder: \(accountHolder)"
}
// ...
}
The nonisolated
keyword is a way to opt-out of the actor isolation, removing the need to access the value using await
.
Solving protocol conformances with nonisolated
The same principle applies to adding protocol conformance in which you’re sure to access immutable state only. For example, we could replace the details property with the nicer CustomStringConvertible
protocol:
extension BankAccount: CustomStringConvertible {
var description: String {
"Account holder: \(accountHolder)"
}
}
Using the default recommended implementation from Xcode, we would run into the following error:
Actor-isolated property ‘description’ cannot be used to satisfy a protocol requirement
Which we can solve again by making use of the nonisolated keyword:
extension BankAccount: CustomStringConvertible {
nonisolated var description: String {
"Account holder: \(accountHolder)"
}
}
The compiler is smart enough to warn us if we accidentally access isolated properties within a nonisolated environment:

Continuing your journey into Swift Concurrency
The concurrency changes are more than just async-await and include many new features that you can benefit from in your code. So while you’re at it, why not dive into other concurrency features?
- Swift Concurrency Course: Modern Concurrency & Swift 6
- What is Structured Concurrency?
- Task.sleep() vs. Task.yield(): The differences explained
- Swift 6: What’s New and How to Migrate
- MainActor usage in Swift explained to dispatch to the main thread
- How to Use URLSession with Async/Await for Network Requests in Swift
- Async await in Swift explained with code examples
- Concurrency-safe global variables to prevent data races
- Unit testing async/await Swift code
- Thread dispatching and Actors: understanding execution
- @preconcurrency: Incremental migration to concurrency checking
- Detached Tasks in Swift explained with code examples
- Task Groups in Swift explained with code examples
- Sendable and @Sendable closures explained with code examples
- AsyncSequence explained with Code Examples
- AsyncThrowingStream and AsyncStream explained with code examples
- Tasks in Swift explained with code examples
- Nonisolated and isolated keywords: Understanding Actor isolation
- Async let explained: call async functions in parallel
- Actors in Swift: how to use and prevent data races
Conclusion
Actors in Swift are a great way to synchronize access to a shared mutable state. In some cases, however, we want to control actor isolation as we might be sure immutable state is accessed only. By making use of the nonisolated and isolated keywords, we gain precise control over actor isolation.
If you like to learn more tips on Swift, check out the Swift category page. Feel free to contact me or tweet me on Twitter if you have any additional suggestions or feedback.
Thanks!