I have a function that executes an async task. Sometimes that task fails and throws an error. I'm having trouble catching that error from the calling function. The playground below captures the essence of the trouble I'm having.
import UIKit
Task {
var newNum: Double = 99.9
do {
newNum = try await getMyNumber()
print("newNum within do: \(newNum)")
} catch MyErrors.BeingStupid { //never gets caught
print("caught being stupid")
} catch MyErrors.JustBecause { //does get caught if throw is uncommented
print("caught just because")
}
print("newNum outside of do \(newNum)")
}
print("done with main")
func getMyNumber() async throws -> Double {
let retNum:Double = 0
Task{
sleep(5)
let myNum: Double = Double.random(in: (0...10))
if myNum > 9 {
print("greater than 9")
} else {
print("less than 9 -- should throw")
throw MyErrors.BeingStupid // error doesn't get thrown? HOW DO I CATCH THIS?
}
}
// throw MyErrors.JustBecause // this *does* get caught if uncommented
return retNum //function always returns
}
enum MyErrors: Error {
case BeingStupid, JustBecause
}
How do I catch the error being thrown at the line commented "HOW DO I CATCH THIS" back in the calling function?
Task
is for unstructured concurrency. If the intent is to simulate an asynchronous task, I would advise remaining within structured concurrency. So, use Task.sleep(nanoseconds:)
instead of sleep()
and eliminate the Task
within getMyNumber
:
func getMyNumber() async throws -> Double {
try await Task.sleep(for: .seconds(5)) // better simulation of some asynchronous process
let myNum = Double.random(in: 0...10)
guard myNum > 9 else {
print("less than or equal to 9 -- should throw")
throw MyErrors.beingStupid
}
print("greater than 9")
return myNum
}
If you stay within structured concurrency, errors that are thrown are easily caught.
For more information about the difference between structured and unstructured concurrency, see The Swift Programming Guide: Concurrency or WWDC 2021 video Explore structured concurrency in Swift
The above illustrates the standard structured concurrency pattern. If you really must use unstructured concurrency, you could try await
the value
returned by the Task
, thereby re-throwing any errors thrown by the Task
, e.g.:
func getMyNumber() async throws -> Double {
let task = Task.detached {
let start = ContinuousClock().now
while start.duration(to: .now) < .seconds(5) {
Thread.sleep(forTimeInterval: 0.2) // really bad idea ... never `sleep` (except `Task.sleep`, which is non-blocking) ... especially never use legacy `sleep` API on the main actor, which is why I used `Task.detached`
try Task.checkCancellation() // but periodically check to see if task was canceled
await Task.yield() // and if doing something slow and synchronous, periodically yield to Swift concurrency
}
let myNum = Double.random(in: 0...10)
guard myNum > 9 else {
print("less than or equal to 9 -- should throw")
throw MyErrors.beingStupid
}
print("greater than 9")
return myNum
}
return try await withTaskCancellationHandler {
try await task.value
} onCancel: {
task.cancel()
}
}
Note, because we’re running something slow and synchronous, we want it to run on a background thread, and therefore use Task.detached
. But, when using unstructured concurrency we would want to properly handle cancelation, namely:
Task
in an withTaskCancellationHandler
because we have to manually handle cancelation in using unstructured concurrency; andyield
to Swift concurrency system.I only include this unstructured concurrency example for the sake of completeness. If you try await
the value
returned by the Task
, errors will be properly propagated.
All of that having been said, it is best to remain within structured concurrency, if you can. That way, you enjoy not only a more concise implementation, but also automatic propagation of cancelation, etc.