Understanding nonisolated in Swift actors?
Swift’s actors provide a powerful way to safely manage concurrent access to a shared, mutable state. By isolating their internal data, actors prevent race conditions and ensure thread safety. However, there are scenarios where certain methods or properties within an actor don’t require this isolation. For such cases, Swift introduces the nonisolated
keyword.
In this article, we’ll dive into what nonisolated
means, why it's useful, and when to apply it to your Swift code. We’ll explore these concepts with a practical example, using an actor called DataProcessor
.

Why Actor Isolation Matters
Before we explore nonisolated
, it’s important to understand actor isolation. Actors enforce isolation to protect their internal mutable state from being accessed or modified from outside the actor's context. This ensures that no two parts of the code can simultaneously modify an actor’s state, which is essential for avoiding race conditions in concurrent programming. This is what actors do which class doesn’t to maintain thread safety.
By default, every access to an actor’s properties or methods must be done asynchronously using await
. This includes both reading from and writing to the actor’s data. Here’s an example:
actor DataProcessor {
var processedData: [String] = []
func process(data: String) {
processedData.append(data)
}
}
In this case, the process(data:)
method modifies the processedData
array, which requires actor isolation. When calling process(data:)
from outside the actor, Swift enforces safe access via await
:
let processor = DataProcessor()
await processor.process(data: "New Data")
This ensures that only one part of the program interacts with the actor’s state at a time. While this is crucial for mutable state, it becomes unnecessary for methods or properties that don’t interact with mutable state.
Enter nonisolated
: Relaxing Unnecessary Isolation
Not all methods inside an actor need to access or modify the actor’s state. Some methods may perform simple calculations, return constant values, or format strings — operations that don’t involve any state changes. Requiring these stateless methods to be called asynchronously via await
introduces unnecessary overhead.
That’s where nonisolated
comes into play. By marking a method or property as nonisolated
, you inform the compiler that it doesn’t need to be isolated within the actor’s context. As a result, you can call it directly from outside the actor without requiring await
.
Let’s extend our DataProcessor
actor with an example:
Example: Stateless Methods in DataProcessor
actor DataProcessor {
var processedData: [String] = []
// This method modifies the actor's state, so it must remain isolated
func process(data: String) {
processedData.append(data)
}
// This method is stateless, so we mark it as nonisolated
nonisolated func description() -> String {
return "DataProcessor is responsible for processing data."
}
// This property is also stateless, so it can be nonisolated
nonisolated var author: String {
return "Swift Developer"
}
}
What Happens with nonisolated
?
In this example, both description()
and author
are marked as nonisolated
. These methods do not interact with any mutable state inside the actor, which means they can be called synchronously, without await
:
let processor = DataProcessor()
// Direct synchronous access to nonisolated methods
print(processor.description()) // Prints: DataProcessor is responsible for processing data.
print(processor.author) // Prints: Swift Developer
Unlike the process(data:)
method, which modifies the processedData
array and requires asynchronous access, the description()
and author
methods are stateless. Therefore, Swift allows them to be accessed without isolation.
Why Use nonisolated
?
The nonisolated
keyword brings several advantages:
- Performance Optimization: When you use
nonisolated
, you avoid the performance overhead of actor isolation. Since these methods don’t require the actor’s state to be thread-safe, there’s no need to switch to the actor’s internal queue. This can lead to performance gains, especially when dealing with frequently accessed methods or properties. - Improved Readability: Marking methods that don’t need isolation as
nonisolated
improves code clarity. It signals to other developers that these methods can be safely accessed outside the actor without worrying about concurrency or async overhead. - Avoiding Unnecessary
await
: For methods that are stateless or return constants, requiringawait
would add unnecessary complexity.nonisolated
simplifies the call site by eliminating the need forawait
where it isn’t required.
When Not to Use nonisolated
While nonisolated
is useful, it’s important to recognize when not to use it. You should never mark methods or properties as nonisolated
if they:
- Access mutable state: Any method that reads or writes to properties that may change over time should remain isolated.
- Interact with the actor’s shared resources: If a method interacts with resources that are used by other methods, isolation is necessary to prevent data races.
For example, in our DataProcessor
actor, the process(data:)
method modifies the processedData
array. It must remain isolated:
actor DataProcessor {
var processedData: [String] = []
// This method modifies the actor's mutable state and needs to be isolated
func process(data: String) {
processedData.append(data)
}
}
Attempting to mark process(data:)
as nonisolated
would violate actor isolation and result in unsafe concurrent access to the actor’s state, potentially causing race conditions.
Conclusion
The nonisolated
keyword in Swift offers a way to optimize and simplify actor-based code by relaxing the need for isolation when it’s unnecessary. It allows methods or properties that don’t interact with mutable state to be accessed synchronously, improving performance and code clarity.
When designing actors in your Swift code, it’s essential to understand the distinction between operations that require actor isolation and those that don’t. By judiciously applying nonisolated
, you can make your actors more efficient while maintaining the concurrency guarantees that actors provide.
Remember:
- Use
nonisolated
for stateless methods and constant properties. - Keep methods that modify or read mutable state isolated to ensure thread safety.
With nonisolated
, you can strike a balance between performance and safety in your Swift concurrency code, making actors even more powerful in your toolkit.