The author felt he achieved nirvana by resolving a few Swift 6 errors, so he got courageous enough to turn on strict concurrency mode for all his projects.

Exploring Swift 6: Static property ‘shared’ is not concurrency-safe because non-‘Sendable’ type may have shared mutable state
Discover how to tackle Swift 6 concurrency errors in this hands-on guide. Learn about Sendable types, @MainActor, and async/await. From @unchecked Sendable to proper concurrency handling, best practices for writing thread-safe Swift code in the new era of strict concurrency checking.

Little did he know that Swift 6 is unfathomable for this young, naive developer.

As he hit the build button, his excitement quickly turned to confusion as Xcode presented him with yet another cryptic warning:

Task-isolated value of type '() async -> ()' passed as a strongly transferred parameter; later accesses could race

This is a new one. Strongly transferred parameter? Later accesses could race? How does Xcode know I am practising for a 5K?

"I thought I had Swift concurrency almost figured out!"


Let's take a closer look at the code that triggered this warning. It is from the widget code I wrote last year using the older @escaping syntax.

public func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
  Task {
    do {
      let entries = try await entriesForTimeline(in: context)
      completion(Timeline(entries: entries, policy: .atEnd))
    } catch {
      let entry = StationEntry(station: nil, color: .red, songs: [], error: error, type: .personal)
      completion(Timeline(entries: [entry], policy: .atEnd))
    }
  }
}

At first glance, this old code seems to be a valid implementation to me. I am creating a new asynchronous task, fetching some entries, and then calling the completion handler with the result. So, what is the problem?

Understanding the Warning

The warning message mentions a "Task-isolated value" being passed as a "strongly transferred parameter", which could lead to "racing accesses".

A Task-isolated value can only be accessed within the context of a specific task. In this case, the completion handler is a closure that captures the context of the task it is defined in.

A strongly transferred parameter is passed to a function in a way that transfers ownership of the value to the called function. This means the caller can no longer access the value after passing it.

Racing accesses occur when multiple threads or tasks try to access the same resource concurrently, leading to unpredictable behaviour.

The warning tells me that I am passing a task-isolated value (the completion handler) as a strongly transferred parameter, which could lead to racing accesses if I try to use it later.

But, why is this a problem? The whole point of the completion handler is to be called later after the entries have been fetched, right?

Task-Isolated Values

When I create a new Task, it is executed on a separate thread or a separate context. This means that any values captured by the task are isolated to that specific task.

In my code, the completion handler is defined inside the Task, so it captures the context of that task. However, I then pass this task-isolated value out of the task by calling the completion handler.

This is where the "strongly transferred parameter" part comes in. When I call the completion handler, I transfer ownership of the task-isolated value to the caller. This means the task can no longer safely access the completion handler because it no longer owns it.

If the task were to try to access the completion handler after calling it, this could lead to racing accesses. The task and the caller would be accessing the same value concurrently, which can wreak havoc on my cute little widget.

That urge to slap a @MainActor to the method is high, but I know I will get another Swift 6-related error that it cannot be used to satisfy the nonisolated protocol requirement. Ugh.

The TimelineProvider Protocol

A quick look at the TimelineProvider protocol before I try to explore a solution:

@available(iOS 14.0, macOS 11.0, watchOS 9.0, *)
@available(tvOS, unavailable)
public protocol TimelineProvider {
  func placeholder(in context: Self.Context) -> Self.Entry

  @preconcurrency func getSnapshot(in context: Self.Context, completion: @escaping @Sendable (Self.Entry) -> Void)

  @preconcurrency func getTimeline(in context: Self.Context, completion: @escaping @Sendable (Timeline<Self.Entry>) -> Void)

The completion handler is marked as @escaping, which means it can be stored and called after the method returns. It is also marked as @Sendable, indicating that it can be safely passed between threads and tasks.

So far, so good. Maybe I can just call the completion handler inside a main actor task?

public func getTimeline(in context: Context, completion: @escaping @Sendable (Timeline<Entry>) -> Void) {
    Task { @MainActor in
      do {
        let entries = try await entriesForTimeline(in: context)
        completion(Timeline(entries: entries, policy: .atEnd))
      } catch {
        let entry = StationEntry(station: nil, color: .red, songs: [], error: error, type: .song)
        completion(Timeline(entries: [entry], policy: .atEnd))
      }
    }
  }

The method already has a @preconcurrency attribute, which tells me that it was designed before Swift's concurrency features were introduced and might not work well with the new concurrency model.

I can quickly silence any other errors now by adding the attribute to the framework, too:

@preconcurrency import WidgetKit

The project compiles, and we can call it a day!

Another Race

I do not think we are done here yet. What was the error that I silenced?

Sending 'completion' risks causing data races

If the completion closure captures any mutable variables or interacts with shared resources, there is a risk that these resources could be accessed or modified concurrently by different tasks, leading to race conditions.

After spending hours on this, I reached out to the Swift veteran Mattiem and went back to sleep to escape reality.

And this is the response I got from him:

What was surprising to me is it cannot ever be a problem to send completion because it is declared @Sendable! This has to be a compiler bug. I was able to reduce that down to an example and I have reported it. swiftlang/swift#76248

The actual problem is you are capturing context and it is not Sendable.

Even if I create a copy of the context and pass it to the task, I still get the same error.


public func getTimeline(in context: Context, completion: @escaping @Sendable (Timeline<StationEntry>) -> Void) {
  Task { @MainActor [context] in
    do {
      let entries = try await entriesForTimeline(in: context)
      completion(Timeline(entries: entries, policy: .atEnd))
    } catch {
      let entry = StationEntry(station: nil, color: .red, songs: [], error: error, type: .personal)
      completion(Timeline(entries: [entry], policy: .atEnd))
    }
  }
}

Moving Forward

As the name of this section suggests, I will move forward in life by working around this compiler bug and adding @preconcurrency import WidgetKit till it is fixed.

Happy Swifting!

Astro Affiliate.

Astro (Affiliate)

Find the right keywords for your app and climb the App Store rankings. Improve your app visibility and increase your revenue with Astro. The first App Store Optimization tool designed for indie developers!

Tagged in: