Published on: January 10, 2025
When you turn on strict concurrency checking or you start using the Swift 6 language mode, there will be situations where you run into an error that looks a little bit like the following:
Main actor-isolated property can not be referenced from a Sendable closure
What this error tells us is that we’re trying to use something that we’re only supposed to use on or from the main actor inside of a closure that’s supposed to run pretty much anywhere. So that could be on the main actor or it could be somewhere else.
The following code is an example of code that we could have that results in this error:
@MainActor
class ErrorExample {
var count = 0
func useCount() {
runClosure {
print(count)
}
}
func runClosure(_ closure: @Sendable () -> Void) {
closure()
}
}
Of course, this example is very contrived. You wouldn’t actually write code like this, but it is not unlikely that you would want to use a main actor isolated property in a closure that is sendable inside of a larger system. So, what can we do to fix this problem?
The answer, unfortunately, is not super straightforward because the fix will depend on how much control we have over this sendable closure.
Fixing the error when you own all the code
If we completely own this code, we could actually change the function that takes the closure to become an asynchronous function that can actually await access to the count property. Here’s what that would look like:
func useCount() {
runClosure {
await print(count)
}
}
func runClosure(_ closure: @Sendable @escaping () async -> Void) {
Task {
await closure()
}
}
By making the closure asynchronous, we can now await our access to count, which is a valid way to interact with a main actor isolated property from a different isolation context. However, this might not be the solution that you’re looking for. You might not want this closure to be async, for example. In that case, if you own the codebase, you could @MainActor
annotate the closure. Here’s what that looks like:
@MainActor
class ErrorExample {
var count = 0
func useCount() {
runClosure {
print(count)
}
}
func runClosure(_ closure: @Sendable @MainActor () -> Void) {
closure()
}
}
Because the closure is now both @Sendable
and isolated to the main actor, we’re free to run it and access any other main actor isolated state inside of the closure that’s passed to runClosure
. At this point count
is main actor isolated due to its containing type being main actor isolated, runClosure
itself is main actor isolated due to its unclosing type being main actor isolated, and the closure itself is now also main actor isolated because we added an explicit annotation to it.
Of course this only works when you want this closure to run on the main actor and if you fully control the code.
If you don’t want the closure to run on the main actor and you own the code, the previous solution would work for you.
Now let’s take a look at what this looks like if you don’t own the function that takes this sendable closure. In other words, we’re not allowed to modify the runClosure
function, but we still need to make this project compile.
Fixing the error without modifying the receiving function
When we’re only allowed to make changes to the code that we own, which in this case would be the useCount
function, things get a little bit trickier. One approach could be to kick off an asynchronous task inside of the closure and it’ll work with count
there. Here’s what this looks like:
func useCount() {
runClosure {
Task {
await print(count)
}
}
}
While this works, it does introduce concurrency into a system where you might not want to have any concurrency. In this case, we are only reading the count
property, so what we could actually do is capture count
in the closure’s capture list so that we access the captured value rather than the main actor isolated value. Here is what that looks like.
func useCount() {
runClosure { [count] in
print(count)
}
}
This works because we’re capturing the value of count when the closure is created, rather than trying to read it from inside of our sendable closure. For read-only access, this is a solid solution that will work well for you. However, we could complicate this a little bit and try to mutate count which poses a new problem since we’re only allowed to mutate count from inside of the main actor:
func useCount() {
runClosure {
// Main actor-isolated property 'count' can not be mutated from a Sendable closure
count += 1
}
}
We’re now running into the following error:
Main actor-isolated property ‘count’ can not be mutated from a Sendable closure
I have dedicated post about running work on the main actor where I explore several ways to solve this specific error.
Out of the three solutions proposed in that post, the only one that would work for us is the following:
Use MainActor.run or an unstructured task to mutate the value from the main actor
Since our closure isn’t async already, we can’t use MainActor.run
because that’s an async function that we’d have to await.
Similar to how you would use DispatchQueue.main.async
in old code, in your new code you can use Task { @MainActor in }
to run work on the main actor:
func useCount() {
runClosure {
Task { @MainActor in
count += 1
}
}
}
The fact that we’re forced to introduce a synchronicity here is not something that I like a lot. However, it is an effect of using actors in Swift concurrency. Once you start introducing actors into your codebase, you also introduce a synchronicity because you can synchronously interact with actors from multiple isolation contexts. An actor always needs to have its state and functions awaited when you access it from outside of the actor. The same applies when you isolate something to the main actor because when you isolate something to the main actor it essentially becomes part of the main actor’s isolation context, and we have to asynchronously interact with main actor isolated state from outside of the main actor.
I hope this post gave you some insights into how you can fix errors related to capturing main actor isolated state in a sendable closure. If you’re running into scenarios where none of the solutions shown here are relevant I’d love if you could share them with me.
Discover more from TrendyShopToBuy
Subscribe to get the latest posts sent to your email.