Solving “Capture of non-sendable type in @Sendable closure” in Swift

Published on: August 7, 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:

Capture of non-sendable type in @Sendable closure

This warning tells us that we're capturing and using a property inside of a closure. This closure is marked as @Sendable which means that we should expect this closure to run in a concurrent environment. The Swift compiler warns us that, because this closure will run concurrently, we should make sure that any properties that we capture inside of this closure can safely be used from concurrent code.

In other words, the compiler is telling us that we're risking crashes because we're passing an object that can't be used from multiple tasks to a closure that we should expect to be run from multiple tasks. Or at least we should expect our closure to be transferred from one task to another.

Of course, there's no guarantees that our code will crash. Nor is it guaranteed that our closure will be run from multiple places at the same time. What matters here is that the closure is marked as @Sendable which tells us that we should make sure that anything that's captured inside of the closure is also Sendable.

For a quick overview of Sendability, check out my post on the topic here.

An example of where this warning might occur could look like this:

func run(completed: @escaping TaskCompletion) {
    guard !metaData.isFinished else {
        DispatchQueue.main.async {
            // Capture of 'completed' with non-sendable type 'TaskCompletion' (aka '(Result<Array<any ScheduledTask>, any Error>) -> ()') in a `@Sendable` closure; this is an error in the Swift 6 language mode
            // Sending 'completed' risks causing data races; this is an error in the Swift 6 language mode
            completed(.failure(TUSClientError.uploadIsAlreadyFinished))
        }
        return
    }

    // ...
}

The compiler is telling us that the completed closure that we're receiving in the run function can't be passed toDispatchQueue.main.async safely. The reason for this is that the run function is assumed to be run in one isolation context, and the closure passed to DispatchQueue.main.async will run in another isolation context. Or, in other words, run and DispatchQueue.main.async might run as part of different tasks or as part of different actors.

To fix this, we need. to make sure that our TaskCompletion closure is @Sendable so the compiler knows that we can safely pass that closure across concurrency boundaries:

// before
typealias TaskCompletion = (Result<[ScheduledTask], Error>) -> ()

// after
typealias TaskCompletion = @Sendable (Result<[ScheduledTask], Error>) -> ()

In most apps, a fix like this will introduce new warnings of the same kind. The reason for this is that because the TaskCompletion closure is now @Sendable, the compiler is going to make sure that every closure passed to our run function doesn't captuire any non-sendable types.

For example, one of the places where I call this run function might look like this:

task.run { [weak self] result in
    // Capture of 'self' with non-sendable type 'Scheduler?' in a `@Sendable` closure; this is an error in the Swift 6 language mode
    guard let self = self else { return }
    // ...
}

Because the closure passed to task.run needs to be @Sendable any captured types also need to be made Sendable.

At this point you'll often find that your refactor is snowballing into something much bigger.

In this case, I need to make Scheduler conform to Sendable and there's two ways for me to do that:

  • Conform Scheduler to Sendable
  • Make Scheduler into an actor

The second option is most likely the best option. Making Scheduler an actor would allow me to have mutable state without data races due to actor isolation. Making the Scheduler conform to Sendable without making it an actor would mean that I have to get rid of all mutable state since classes with mutable state can't be made Sendable.

Using an actor would mean that I can no longer directly access a lot of the state and functions on that actor. It'd be required to start awaiting access which means that a lot of my code has to become async and wrapped in Task objects. The refactor would get out of control real fast that way.

To limit the scope of my refactor it makes sense to introduce a third, temporary option:

  • Conform Scheduler to Sendable using the unchecked attribute

For this specific case I have in mind, I know that Scheduler was written to be thread-safe. This means that it's totally safe to work with Scheduler from multiple tasks, threads, and queues. However, this safety was implemented using old mechanisms like DispatchQueue. As a result, the compiler won't just accept my claim that Scheduler is Sendable.

By applying @unchecked Sendable on this class the compiler will accept that Scheduler is Sendable and I can continue my refactor.

Once I'm ready to convert Scheduler to an actor I can remove the @unchecked Sendable, change my class to an actor and continue updating my code and resolving warnings. This is great because it means I don't have to jump down rabbit hole after rabbit hole which would result in a refactor that gets way out of hand and becomes almost impossible to manage correctly.

Subscribe to my newsletter