Fixing “Essential actor-isolated property can’t be referenced from a Sendable closure” in Swift – Donny Wals

Fixing “Essential actor-isolated property can’t be referenced from a Sendable closure” in Swift – Donny Wals


Printed on: January 10, 2025

Whenever you activate strict concurrency checking otherwise you begin utilizing the Swift 6 language mode, there shall be conditions the place you run into an error that appears a bit of bit like the next:

Essential actor-isolated property can’t be referenced from a Sendable closure

What this error tells us is that we’re making an attempt to make use of one thing that we’re solely supposed to make use of on or from the primary actor within a closure that is imagined to run just about wherever. In order that might be on the primary actor or it might be someplace else.

The next code is an instance of code that we may have that outcomes on this error:

@MainActor
class ErrorExample {
  var rely = 0

  func useCount() {
    runClosure {
      print(rely)
    }
  }

  func runClosure(_ closure: @Sendable () -> Void) {
    closure()
  }
}

After all, this instance could be very contrived. You would not really write code like this, however it’s not unlikely that you’d wish to use a primary actor remoted property in a closure that’s sendable inside of a bigger system. So, what can we do to repair this downside?

The reply, sadly, will not be tremendous easy as a result of the repair will rely on how a lot management now we have over this sendable closure.

Fixing the error while you personal all of the code

If we utterly personal this code, we may really change the perform that takes the closure to turn into an asynchronous perform that may really await entry to the rely property. This is what that may appear like:

func useCount() {
  runClosure {
    await print(rely)
  }
}

func runClosure(_ closure: @Sendable @escaping () async -> Void) {
  Activity {
    await closure()
  }
}

By making the closure asynchronous, we will now await our entry to rely, which is a sound method to work together with a primary actor remoted property from a unique isolation context. Nevertheless, this won’t be the answer that you simply’re searching for. You won’t need this closure to be async, for instance. In that case, for those who personal the codebase, you might @MainActor annotate the closure. This is what that appears like:

@MainActor
class ErrorExample {
  var rely = 0

  func useCount() {
    runClosure {
      print(rely)
    }
  }

  func runClosure(_ closure: @Sendable @MainActor () -> Void) {
    closure()
  }
}

As a result of the closure is now each @Sendable and remoted to the primary actor, we’re free to run it and entry every other primary actor remoted state within the closure that is handed to runClosure. At this level rely is primary actor remoted because of its containing sort being primary actor remoted, runClosure itself is primary actor remoted because of its unclosing sort being primary actor remoted, and the closure itself is now additionally primary actor remoted as a result of we added an express annotation to it.

After all this solely works while you need this closure to run on the primary actor and for those who absolutely management the code.

If you don’t need the closure to run on the primary actor and also you personal the code, the earlier resolution would give you the results you want.

Now let’s check out what this seems like for those who do not personal the perform that takes this sendable closure. In different phrases, we’re not allowed to switch the runClosure perform, however we nonetheless have to make this mission compile.

Fixing the error with out modifying the receiving perform

Once we’re solely allowed to make modifications to the code that we personal, which on this case could be the useCount perform, issues get a bit of bit trickier. One method might be to kick off an asynchronous process within the closure and it will work with rely there. This is what this seems like:

func useCount() {
  runClosure {
    Activity {
      await print(rely)
    }
  }
}

Whereas this works, it does introduce concurrency right into a system the place you won’t wish to have any concurrency. On this case, we’re solely studying the rely property, so what we may really do is seize rely within the closure’s seize record in order that we entry the captured worth fairly than the primary actor remoted worth. Here’s what that appears like.

func useCount() {
  runClosure { [count] in
    print(rely)
  }
}

This works as a result of we’re capturing the worth of rely when the closure is created, fairly than making an attempt to learn it from within our sendable closure. For read-only entry, this can be a stable resolution that can work properly for you. Nevertheless, we may complicate this a bit of bit and attempt to mutate rely which poses a brand new downside since we’re solely allowed to mutate rely from within the primary actor:

func useCount() {
  runClosure {
    // Essential actor-isolated property 'rely' can't be mutated from a Sendable closure
    rely += 1
  }
}

We’re now working into the next error:

Essential actor-isolated property ‘rely’ can’t be mutated from a Sendable closure

I’ve devoted submit about working work on the primary actor the place I discover a number of methods to resolve this particular error.

Out of the three options proposed in that submit, the one one that may work for us is the next:

Use MainActor.run or an unstructured process to mutate the worth from the primary actor

Since our closure is not async already, we won’t use MainActor.run as a result of that is an async perform that we would should await.

Just like how you’d use DispatchQueue.primary.async in outdated code, in your new code you should use Activity { @MainActor in } to run work on the primary actor:

func useCount() {
  runClosure { 
    Activity { @MainActor in
      rely += 1
    }
  }
}

The truth that we’re pressured to introduce a synchronicity right here will not be one thing that I like rather a lot. Nevertheless, it’s an impact of utilizing actors in Swift concurrency. When you begin introducing actors into your codebase, you additionally introduce a synchronicity as a result of you may synchronously work together with actors from a number of isolation contexts. An actor at all times must have its state and features awaited while you entry it from outdoors of the actor. The identical applies while you isolate one thing to the primary actor as a result of while you isolate one thing to the primary actor it primarily turns into a part of the primary actor’s isolation context, and now we have to asynchronously work together with primary actor remoted state from outdoors of the primary actor.

I hope this submit gave you some insights into how one can repair errors associated to capturing primary actor remoted state in a sendable closure. Should you’re working into eventualities the place not one of the options proven listed here are related I would love for those who may share them with me.

Leave a Reply

Your email address will not be published. Required fields are marked *