After a false start, @ktoso and I are back at it. We'd would like to make another attempt at adopting typed throws in the Task creation APIs and make it more difficult to ignore thrown errors accidentally. The spirit of the changes are identical to the previous proposal, but have been expanded to accommodate all the new Task APIs.
Just to recap:
The motivation for adopting typed throws can be clearly illustrated with a code example:
let task = Task {
throw MyError.somethingBadHappened
}
do {
_ = try await task.value
} catch {
// type information has been lost and error is now `any Error`
}
Additionally, all the Task creation APIs are annotated with @discardableResult, including those that permit failure. This makes it extremely easy for the code creating the task to unintentionally ignore errors thrown in the body. This default has proven to be surprising and leads to accidentally missing thrown errors, as documented in this thread.
The typed throws adoption is pretty much a no brainer, however in this pitch thread it’d be good to revisit the removal of @discardableResult on error throwing functions.
This was discussed at length in various places, but now that we’re actually proposing it it’d be good to hear if anyone has concerns about it in this pitch (or later, in the proposal review).
I don’t think we’re in the business of “you took a task handle, but forgot to await on it, so you dropped the value or error”. It becomes even harder to diagnose if you escaped the handle for example…
In order to “cannot forget to await a task” you should be using structured concurrency. I see that your example the foo() is synchronous, but I don’t think there’s any reasonable diagnostics to invent here – how could the compiler tell, and what would you even do since you cannot await the task’s value. You’d have to handle the error inside the Task, so building diagnostics forcing you to do that would mean inventing some ways to effectively force you to await or never throw in a Task{} that you did not await on – that’s very undefined and hard to track in the general sense…
Yeah, you are probably right. I am thinking of some hypothetical infrastructure where errors are some kind of tag's "it" that you can't just ignore, or drop away, you could only pass it somewhere, if nothing better then into some terminal printItInBigRedBoldOnTheConsole(_ error: Error) API.
func bar() {
let task = Task {
throw MyError.somethingBadHappened
}
print(task) // or task.cancel() after a small delay
// ❌ Error: Errors must be handled
do {
let x = try foo()
print(x)
} catch {
// do nothing here
// ❌ Error: Errors must be handled
}
let r = Result { try foo() }
// do nothing with it
// ❌ Error: Errors must be handled
}
Although this seems to be outside of this pitch scope.
+1 for me on dropping the @dicardableResultdicardableResult when Failure is not Never, I think it’s a better default. It might also be a good idea when Success is not Void.
Typed throw seems pretty straightforward, even if a bit theoretical to me because I haven’t use typed throws yet.
I really miss not having exact typed exceptions and the ability to declare the type thrown exactly etc., that Java provides. It’s amazing how many thing Java got right and how so many people complained and/or ignored the power of typed exceptions. Here we are 30 years later and suddenly everyone is rediscovering what Java has provided for decades and wants similar functionality.
Any thread of execution that throws an exception should print any unhandled exception out. Silent exception ignoring should not be possible. This is a code bug, and it should be visible to the developer if not actual user with a popup dialog/sheet/something for SwiftUI applications
Originally we intended this to be just about the Task.init and friends, but we could consider doing the withValue one’s well here perhaps… they’re pretty related.
Just wanted to let everyone know that, after quite a bit of back and forth, this change ended up not needing to go through the proposal process. It was finished up recently and just merged.
Unfortunately, TaskLocal support didn’t make it as part of this work. But, I’m planning on continuing to look at it.