Let me start by saying we will cover an advanced use case of concurrency. In most projects, you won’t be using the #isolation macro, but I still want to cover it since it can be a way to write more generic concurrency code.
What is the #isolation macro?
In Swift’s actor design, each function is either tied to a specific actor or non-isolated. Sometimes, it’s helpful for a function to inherit the actor isolation from its caller—either to access data that’s actor-isolated or simply to skip unnecessary suspensions. The #isolation macro offers a way for async functions to opt into that behavior.
The #isolation macro gives async functions the ability to inherit the actor isolation of their caller. Normally, async functions reset isolation, leading to unnecessary suspensions and limiting the ability to work with non-sendable data in actor-isolated contexts. By opting into the caller’s isolation using #isolation, you can safely pass non-sendable values and avoid performance overhead from context switching.
How to use the #isolation macro
Imagine a scenario where we want to iterate over an array of names and asynchronously lowercase each. It’s an example that works great for this explanation, but you can imagine it representing some kind of remote name formatter.
We want to write a generic extension on Collection, so we can reuse the logic in the future. We also want to sequentially iterate over each name. The code looks as follows:
extension Collection where Element: Sendable {
func sequentialMap<Result: Sendable>(
transform: (Element) async -> Result
) async -> [Result] {
var results: [Result] = []
for element in self {
results.append(await transform(element))
}
return results
}
}
The function takes a transform closure with support for asynchronous calls. It iterates over each element sequentially and returns a new collection of transformed values. This generic function is exactly what we need for our scenario.
Let’s try to use the code as follows:
Task { @MainActor in
let names = ["Antoine", "Maaike", "Sep", "Jip"]
let lowercaseNames = await names.sequentialMap { name in
await lowercaseWithSleep(input: name)
}
print(lowercaseNames)
}
You might think: all looks good; lesson closed! However, it demonstrates the issue we’re going to solve with the #isolation macro:

Mapping asynchronously over values isn’t as easy as you might think.
The sequentialMap function steps out of the @MainActor isolation, meaning we’re going to call into the transform closure from another isolation domain. This results in potential data races, causing the compiler to complain.
We can solve this by making our map function inherit the actor isolation. However, we can’t simply use @MainActor since we decided to write a generic solution. We can solve this by using the #isolation macro:
func sequentialMap<Result: Sendable>(
isolation: isolated (any Actor)? = #isolation,
transform: (Element) async -> Result
) async -> [Result] {
var results: [Result] = []
for element in self {
results.append(await transform(element))
}
return results
}
We’ve learned about the isolated parameters in the previous lesson, and we’re now taking it one step further by making it default to the current actor isolation that applies. This is done via the default argument that uses #isolation.
This shows the power of actor inheritance, which can be the solution in cases where you want to write a more generic concurrency code.
Summary
Actor inheritance can help you write generic concurrency code that performs logic on the current actor isolation. It prevents unnecessary suspensions, allows you to safely pass non-sendable values, and avoids performance overhead from context switching.
In the next lesson, we will look into using a custom actor executor.