How do I solve "task-isolated value...passed as strongly transferred parameter" warning?

I'm in Xcode 16 beta 5. I've got this code:

@objc class func fetch(product: StoreListProduct, completion: @escaping @Sendable (AppStoreProduct?) -> Void) {
        Task {
            let result = await AppStoreProduct.product(forListProduct: product)
            DispatchQueue.main.async {
                completion(result)
            }
        }
    }

For context:

  • I am moving to StoreKit 2 in a largely Objective-C codebase.
  • StoreListProduct is an Objective-C class that wraps an NSDictionary built from json fetched from my server describing a product in my store, and exposes various keys in that dictionary as typed properties for convenience.
  • AppStoreProduct is a Swift class wrapping the StoreKit 2 Product class and exposing various parts of it to Objective-C. It's marked as Sendable.
  • AppStoreProduct.product(forListProduct:) is a Swift method that ultimately just wraps the Product.products method in StoreKit 2.

I'm getting this warning on the Task { line:

Task-isolated value of type '() async -> ()' passed as a strongly transferred parameter; later accesses could race; this is an error in the Swift 6 language mode

My best interpretation of this is that the closure being passed to the Task is considered to be task-isolated because it's capturing the non-Sendable StoreListProduct, so the closure can't be transferred across the isolation boundary. But I'm a little confused by the warning's reference to "later accesses". The closure is defined locally once and not stored.

In practice, I believe this is safe because StoreListProduct is immutable.

What's the best way to mark this as (or make it) safe?

2 Likes

Thanks for this question. To give you a clue on how I (think!) I figured this out, I had to use your information to reconstruct the types involved. The types are all that matter here!

import Foundation

// a non-Sendable type
class StoreListProduct: NSObject {

}

final class AppStoreProduct: NSObject, Sendable {
    class func product(forListProduct: StoreListProduct) async -> AppStoreProduct? {
        // the body here does not matter, just the signature
        return nil
    }
}

class Container {
    @objc class func fetch(product: StoreListProduct, completion: @escaping @Sendable (AppStoreProduct?) -> Void) {
        // I'm not sure what the isolation here is, but I'm assuming nothing
        Task {
            // the compiler is telling us that it cannot pass this closure out to Task, because it cannot send it.
            // I believe the reason *why* is becuase we've captured product, but it doesn't know how/when product is
            // used by the caller and it is not Sendable, making a transfer unsafe.
            let result = await AppStoreProduct.product(forListProduct: product)

            // and here are moving result into the MainActor
            DispatchQueue.main.async {
                completion(result)
            }
        }
    }
}

So, what's a solution? We have to somehow allow that product variable to transfer over to that Task. You have some options.

option 1: explicitly allow the transfer (Swift 6-only)

By using sending, we are telling the compiler that all call-sites must allow a transfer. This fixes the error, but adds extra constraints on the call-sites that may or may not be ok.

@objc class func fetch(
    product: sending StoreListProduct,
    completion: @escaping @Sendable (AppStoreProduct?) -> Void
)

option 2: mark everything MainActor

This is Swift 5-compatible, and probably a reasonable solution. Plus, by doing this, you can avoid an additional hop and add more explicitness to your API.

class Container {
    @MainActor
    @objc class func fetch(
        product: StoreListProduct,
        // unforutnately, in Swift 5, this closure must *also* be marked @Sendable
        completion: @escaping @MainActor (AppStoreProduct?) -> Void
    ) {
        Task {
            let result = await AppStoreProduct.product(forListProduct: product)

            completion(result)
        }
    }
}
1 Like

Just to provide some context.

Task-isolated means that there may still be references in the current task (specifically in callers of your function). So imagine passing the value over to another Task and your caller then using it.

You are correct that the reason why the closure is task-isolated is because it is capturing the function parameter.

The later uses is referring to that there could be later uses of things in the region. It may not be the closure... it could be anything in the region (for instance in this case, the function parameter that the closure captures).

The reason that sending eliminates the problem is that by marking the parameter sending, you are telling the compiler to restrict my callers from keeping the function parameter around. So you know that even though the function parameter is in your task, no one but your parameter will still have access to it, so you can send it into the task without worrying.

2 Likes

I think this error message is really hard to understand in the current form. Not only the overall structure, but also the fact it complains about closure transferring, while it is not related to the closure exactly, plus transferred isn’t clear term IMO.

4 Likes

Thanks for this!

And, you're now reminding me. What does the "strongly" mean here? I guess there other forms of transfer?

It's some region-based isolation terminology.

1 Like

I don't think the term "strongly" means anything insightful in the user-facing language semantics around this error. It should be removed from the error message and programmers should not feel like they need to understand what it means.

6 Likes

Thanks for all the input, everyone!

For a little more clarity on StoreListProduct, it looks like this:

//header file:
@interface StoreListProduct : NSObject
-(id)initWithData:(NSDictionary *)data;
@property (readonly) NSInteger productId;
//...and more such properties, all read-only
@end

//in the .m file:
@interface StoreListProduct ()
@property (strong) NSDictionary *data;
@end

@implementation StoreListProduct

-(id)initWithData:(NSDictionary *)data
{
  self = [super init];
  self.data = data;
  return self;
{

-(NSInteger)productId
{
  NSNumber *num = self.data[@"product_id"];
  if ([num isKindOfClass:[NSNumber class]])
    return num.integerValue;

  return 0;
}

@end

In short, it wraps a read-only dictionary and exposes various data in that dictionary as typed properties, all read-only. Technically speaking I could reset the data property internally - it's not like a let in Swift - but as long as I don't, this class is thread-safe.

Given this, I tried marking it as Sendable by amending the @interface declaration in the header like so:

__attribute__((swift_attr("@Sendable")))
@interface StoreListProduct : NSObject

And the warning went away.

Is there any reason it is not a good idea to mark an Objective-C class this way, other than that the compiler won't warn me if I ever screw up and break the thread safety internally somehow? I do hope to convert this class to Swift eventually anyway.