Solving “Task-isolated value of type ‘() async -> Void’ passed as a strongly transferred parameter”

Published on: August 21, 2024

Once you start migrating to the Swift 6 language mode, you'll most likely turn on strict concurrency first. Once you've done this there will be several warings and errors that you'll encounter and these errors can be confusing at times.

I'll start by saying that having a solid understanding of actors, sendable, and data races is a huge advantage when you want to adopt the Swift 6 language mode. Pretty much all of the warnings you'll get in strict concurrency mode will tell you about potential issues related to running code concurrently. For an in-depth understanding of actors, sendability and data races I highly recommend that you take a look at my Swift Concurrency course which will get you access to a series of videos, exercises, and my Practical Swift Concurrency book with a single purchase.

WIth that out of the way, let's take a look at the following warning that you might encounter in your project:

Task-isolated value of type '() async -> Void' passed as a strongly transferred parameter

When I first encountered the error above, I was puzzled. The code that made this happen wasn't all that strange and I had no idea what could be wrong here.

Let's look at an example of the code that would make this error show up:

var myArray = [1, 2, 3]

await withTaskGroup(of: Void.self) { group in
  for _ in 0..<10 {
    // Task-isolated value of type '() async -> Void' passed as a strongly transferred parameter; later accesses could race;
    group.addTask { 
      myArray.append(Int.random(in: 0..<10))
    }
  }
}

The problem above can also occur when you create an unstructured task with Task or a detached task with Task.detached. The error and the reason for the error appearing are the same for all cases, but what exactly is wrong in the code above?

Unfortunately, the compiler isn't of much help here so we'll need to figure this one out on our own...

In every case that I've seen for this specific error, the task that we create (whether it's a child task, unstructured task or a detached task) captures a non-sendable object. To learn more about sendable, take a look at my post that explains Sendable and @Sendable closures.

So while the compiler error is extremely hard to read and understand, the reason for it appearing is actually relatively simple. We've got a strong capture to something that's not Sendable inside of a task that might run concurrently with other work. The result is a possible data race.

The fix can sometimes be relatively simple if you're able to make the captured type sendable or an actor. In the case of the code above that would be tricky; myArray is an array of Int which means that we're already as sendable as we could be. But because the array is mutable, there's a chance that we'll race.

There are multiple possible fixes in this case. One of them is to mutate the array outside of the child tasks by having child tasks produce numbers and then iterating over the task group:

var myArray = [1, 2, 3]

await withTaskGroup(of: Int.self) { group in
  for _ in 0..<10 {
      group.addTask {
          // Task-isolated value of type '() async -> Void' passed as a strongly transferred parameter; later accesses could race;
          return (myArray.first ?? 2) * 2
      }
  }

    for await value in group {
        myArray.append(value)
    }
}

Unfortunately, the above still produces an error...

The reason for that is that myArray is still being accessed from within a child task. So that means that while a child task is reading, our async for loop could be writing and then we have a data race.

To fix that we need to make a copy of myArray in the child task's capture list like this:

group.addTask { [myArray] in
  return (myArray.first ?? 2) * 2
}

With that change in place, the code compiles and runs correctly.

Unfortunately, Task-isolated value of type '() async -> Void' passed as a strongly transferred parameter is a very tough to read error with no single fix. What this error tells you though, is that you're accessing or capturing a value that's not sendable or safe to be accessed concurrently. Fixes for this could be:

  1. To make the captured object an actor
  2. To make the captured object sendable
  3. To make a copy of the object
  4. To capture properties on the object outside of your task
  5. To rethink your approach completely (this is rarely needed)

As with many other strict concurrency related issues, solving this error will depend on your ability to analyze the problem, and your understanding of actors and sendable. These are topics that you should try and understand as good as you can before you attempt to migrate to Swift 6.

Subscribe to my newsletter