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.
It is so weird that Task.init and .detached just create things which don't require any more methods to start running. Doesn't feel like any other Swift API.
I think the best compromise* for the conflation of Task and Task.value that stems from that is to remove @discardableResult from everything except Task<Void, Never> overloads, but along with that, add this, to silence the warnings:
* Unless we can get new warnings, instead. Accurate warnings with the current discardable results would be better than the inaccurate Expression of type 'Task' is unused that will result from what's been proposed.
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.
While I disagree with this equality, because a Task is not its own value, I'm still for the change, because nothing good and true is available.
discardableResult being involved at all is a hack, attempting to cover up the missing ability to annotate a type as a wrapper around a result, delaying the applicability of where we currently use discardableResult:
@resultWrapper struct Task⦠{
@result var value: Successā¦
@result var result: Resultā¦
var notAResult: Successā¦
@discardResult func discardResult() { }
@discardableResult func notADiscard() { }
}
Task { "value or throw" } // Warning: "Result not handled" or something
await Task { "value or throw" }.notAResult // either the same warning or a related one.
Task { "value or throw" }.notADiscard() // either the same warning or a related one.
await Task { "value or throw" }.value // no warning
Task { "value or throw" }.discardResult() // no warning