Async Task requiring any Sendable return

Let's say I have 2 async tasks, one that keeps a ref, and one that doesn't:

var testTask: Task<Sendable, Error>?

func runFeedProcess() {

  // This task just runs; no ref to the task is saved.
  Task {
    print("test1")
  }

  // This task stores a ref to the task in a var.
  self.testTask = Task {
      print("test2")
  }
}

These both compile and run fine. So far so good.

Now, let's say I add a do/catch to them:

var testTask: Task<Sendable, Error>?

enum DataError: Error {
    case missingNumber
}
  func divideNumBy2(num:Int?) throws -> Int {
    guard let unwrappedNum = num else {
      throw DataError.missingNumber
    }
      return unwrappedNum / 2
  }

func runFeedProcess() {

  // This task just runs; no ref to the task is saved.
  Task {
    print("test1")

    do {
      let result = try divideNumBy2(num: 10)
    }
    catch DataError.missingNumber{
      print("missing number")
    }
  }

  // This task stores a ref to the task in a var.
  self.testTask = Task {
      print("test2")

    do {
      let result = try divideNumBy2(num: 10)
    }
    catch DataError.missingNumber{
      print("missing number")
    }
  }  // ERROR HERE--"Missing return in closure expected to return 'any Sendable'"
}
  1. Why is the Task whose ref is being saved complaining about this now that there's a do/catch in there? But it didn't complain when there wasn't the do/catch?
  2. Why doesn't the non-saved Task complain?
  1. Because of implicit returns from single-expression functions.
  2. Because the return type is implicitly inferred as Void if you do not specify a type. If the return type is Void you do not need to explicitly return.

To fix the error you have two options.

  1. add a return () to the last line of your function body. This returns Void explicitly and Void conforms to Sendable and makes this already valid. You could also return an Int or any other type which conforms to Sendable.
  2. better: change the Success type of var testTask: Task<Sendable, Error>? to Void i.e. Task<Void, Error>?
2 Likes

(post deleted by author)

Ahhh, this is fascinating--thank you for explaining all of this! It's much less mysterious now.

1 Like

Really appreciate the response! Wasn't sure if you saw my last post because it got false-flagged as spam.

1 Like

You are welcome!

Can you still remember how you end up with Task<Sendable, Error>? If autocomplete suggest that as a type this might be, IMHO, a bug and should instead default to Void and not to Sendable.

I did a right-click inside Task and selected "Show Quick Help", and got this (pic below), so I figured the declaration needed to match that. I'm guessing that's correct? I would never have realized that Void is a valid Sendable too, though, unless you'd told me (which is very handy to know).
I had read the definition of Sendable here:
https://developer.apple.com/documentation/swift/sendable

Screen Shot 2022-10-05 at 9.55.41 PM

Yeah that is not obvious... Void is just a typealias to the empty tuple () and tuples can normally not conform to protocols. Only the compiler can synthesis conformance for tuples which it does for Sendable, Equatable and Hashable if all elements conform to the protocol in question.

The types don't need to match exactly if the constraint uses a colon (where Success : Sendable, Failure : Error) which just means that the type need to conform to that protocol after the colon but can be any type that satisfies this constraint. Only if the constrains uses the equal sign (e.g. where Success == Int) the type must be exactly the same. This wouldn't make sense in that case because Success wouldn't need to be generic. In fact the compiler will emit a warning if you try to define a type like that e.g.:

struct Foo<T> where T == Int {} // Same-type requirement makes generic parameter 'T' non-generic

Note that also the Failure type doesn't need to be Error. You can use Never as the Error type if you want that the Task can never fail. Never is a type which can not be constructed. The compiler has special knowledge about that type and therefore you can wait for the task completion without writing try e.g.:

let task1: Task<Void, Never> = Task { }
await task1.value // no need for `try`

let task2: Task<Void, Error> = Task { }
try await task2.value // try required because we said the Task can throw
2 Likes

Wow, this is blowing my mind but also fascinating. Thanks for explaining all this. I have to read through this a few times to fully comprehend, but I'm glad for getting a deeper understanding of what's going on under the hood.

1 Like