Logo for tanaschita.com

Understanding actors in Swift

Learn how to use Swift actors to prevent data races when working with concurrency.

28 Oct 2024 · 7 min read

Concurrency is a complex problem when writing software, especially when dealing with shared mutable state across different threads. With Swift's move toward structured concurrency, actors are a powerful feature introduced to make it easier to write safe and efficient concurrent code.

In this article, we'll dive into what actors are, how they differ from other types in Swift, and how they can help you avoid data races in concurrent environments.

Sponsorship logo
Architecture & Design Patterns for iOS
This book is a practical guide to essential architectural principles and design patterns for iOS development. It covers strategies for building maintainable apps with Swift and SwiftUI, including dependency injection, navigation, common patterns, and modularization.
LEARN MORE

How do Swift actors prevent data races?

A data race occurs when multiple threads access shared mutable data concurrently, and at least one thread modifies the data. This leads to unpredictable results, bugs, and crashes.

Swift's actors prevent data races through actor isolation, which ensures:

  • Only one task can access or modify an actor’s mutable state at any given time.
  • Synchronous access to an actor's mutable state from outside is prohibited.
  • Direct modification of an actor's mutable state from outside is not allowed.

By using actors, Swift guarantees that shared mutable data is safely managed in a concurrent environment, reducing the risk of data races.

Defining an actor

Actors are represented by a reference type called actor:

actor Shop {
let id = "abc"
var itemsCount = 10
func purchase() {
itemsCount-=1
}
}

In this example, Shop is an actor that holds a mutable state itemsCount. The actor ensures that only one task can modify itemsCount at a time. This is the core advantage of actors over classes when it comes to handling concurrent operations.

Using async/await to access data from an actor

To interact with an actor's properties or methods from outside, we need to use async/await, marking any potential suspension points with await. Only immutable data, such as constants (id in this case), can be accessed synchronously.

let shop = Shop()
print(shop.id) // Synchronous access to immutable property
Task {
await shop.purchase() // Asynchronous call to modify mutable state
let count = await shop.itemsCount // Asynchronous access to mutable state
}

Even though we didn't explicitly mark purchase as an async function, we still need to use await when calling it because it interacts with the actor's mutable state. If some other task has already called purchase, our purchase() call would suspend and wait for the other one to finish.

This is part of actor isolation, which ensures that only asynchronous access to an actor's mutable state is allowed from outside.

Actor isolation

Actor isolation is the principle that protects an actor's internal state. So when accessing the data of an actor, the following rules apply:

  • Synchronous access to an actor's mutable state from the outside is not allowed.
  • Asynchronous access via await is required for mutable state.
  • Reading immutable properties (constants) from the outside can be done synchronously.
  • Internal access (within the actor’s methods) can happen synchronously.

These guarantees ensure that concurrent access to shared data is controlled and safe and are known as actor isolation.

Actor's non-isolated declarations

There may be cases where we want to allow synchronous access to certain methods or properties of an actor. For this, Swift provides the nonisolated keyword, which allows a method or property to bypass actor isolation.

actor Shop {
let id = "abc"
nonisolated func log(message: String) {
print("\(id): \(message)")
}
}

In this example, the log method is declared nonisolated, allowing it to be called synchronously from outside the actor without await. Inside the nonisolated method, we are only allowed to use nonisolated properties (constants) and methods.

Handling thread safety with Sendable types

In Swift, actors are designed to protect their internal state from concurrent access, but we still need to be careful when passing data in and out of an actor. This is where the Sendable protocol comes into play. Sendable ensures that any type used in a concurrent environment is thread-safe, meaning it can safely be shared across different tasks without causing data races.

So far, we have only used value types String or Int when working with actors. By default, Swift's value types like Int and String are Sendable, but when working with custom types, it's important to ensure that they conform to Sendable.

To check the thread safety of a type we can use the Sendable protocol:

actor Shop {
var owner: Owner
}
struct Owner: Sendable {
var name: String
}

Mutable classes are not thread safe:

class Owner: Sendable { // compiler error: Non-final class 'Owner' cannot conform to Sendable
var name: String
}

Since mutable classes are not thread safe we get a compiler error. To avoid this, we should either mark immutable classes as final or prefer using structs, which are value types and inherently thread-safe.

Actor reentrancy

One unique feature of Swift actors is reentrancy. When an actor is suspended awaiting an async call, it can process other tasks in its queue. This improves efficiency but can lead to unexpected behavior if we’re not careful, as an actor’s state may change between async calls. Swift automatically manages reentrancy, but it’s important to be aware of this behavior when working with actors.

Sponsorship logo
Preparing for a technical iOS job interview
Preparing for a technical iOS Job Interview with over 300 questions & answers. Covering Swift & Objective-C, SwiftUI & UIKit, Combine, HTTP Networking, iOS File System, Core Data, Concurrency with async/await, Security, Automated Testing, Dependency Management, AI & Machine Learning and more.
LEARN MORE
Sponsorship logo
Architecture & Design Patterns for iOS
This book is a practical guide to essential architectural principles and design patterns for iOS development. It covers strategies for building maintainable apps with Swift and SwiftUI, including dependency injection, navigation, common patterns, and modularization.
LEARN MORE
Sponsorship logo
Become a sponsor of tanaschita.com
By publishing an article on different iOS topics every week, tanaschita.com is constantly growing in the developer community and may provide a great audience for you as a sponsor.
CLICK TO LEARN MORE

Newsletter

Image of a reading marmot
Subscribe

Like to support my work?

Say hi

Related tags

Articles with related topics

async/await

concurrency

swift

Understanding Sendable in Swift

Learn what Sendable means in Swift concurrency and how it helps prevent data races.

06 Jul 2026 · 8 min read

Latest articles and tips

© 2026 tanaschita.com

Privacy policy

Impressum