SE-0413: Typed throws

And this here is a concise summary of why functional programming with parametric polymorphism is good—instead of reasoning about the dynamic execution of your program, you define your types so that the only possible way to inhabit them already satisfies your invariants.

Dynamic casting can fail, but as long as your function is total, you can only throw a thing you caught. This is expressed in the type signature, without the compiler having the reason about execution (which in the case of rethrows we know it does incorrectly).

18 Likes

Would it be substantially more expensive to add the minimum of logic to handle the example given above? That is:

do {
  try callCat() // throws CatError
  try callKids() // throw KidError
} catch is CatError {
    print("Error was CatError")
} catch is KidError {
    print("Error was KidError")
}

I would imagine that from an end-user perspective the lack of heroic efforts to check exhaustivity is totally understandable, but the lack of any attempt to accommodate any more than one type might be rather more surprising.

Of course, I think it's also fair to point out that the end user's code can be trivially rewritten:

// ...
catch is CatError {
    print("Error was CatError")
} catch {
    // assert(error is KidError)
    print("Error was KidError")
}
2 Likes

Indeed, and I was picturing a different trivial rewrite:

do {
    try callCat() // throws CatError
} catch is CatError {
    print("Error was CatError")
}
do {
    try callKids() // throw KidError
} catch is KidError {
    print("Error was KidError")
}

In most situations, I’d think that splitting up the error handling like this is something we’d want to encourage.

Is there a compelling reason for more compiler magic to allow people to avoid that? Scoping inconveniences with do-local variables, maybe, where it becomes necessary to declare a local variable outside the do/catch block and/or nest the do/catch blocks inside a larger do block? There might be something there, though I’m skeptical of whether the problem is common enough or serious enough to warrant language gymnastics.

Edit: I suppose the more serious motivation would be control flow — and my own rewrite above shows that. :person_facepalming: In a situation where you want the earlier error to bypass the later code but still be exhaustively handled, the alternatives to the aggregated catch block is another pyramid of doom:

do {
    try callCat() // throws CatError
    do {
        try callKids() // throw KidError
    } catch is KidError {
        print("Error was KidError")
    }
} catch is CatError {
    print("Error was CatError")
}
2 Likes

Very happy that the "normal" use of rethrows just falls out of the type system with this proposal. My reaction to the typed rethrows alternative discussion section was a definite "Eww", as I have always thought of rethrows as being only for trying a closure param invocation and failing to catch around it. I'd prefer to always mark a func as throws (not rethrows) if it is creating its own errors, regardless of whether it is only doing it in a catch block.

All this to say, even if you don't plan on doing another proposal regarding rethrows any time soon, I would greatly welcome an experimental feature that performs the syntactic sugar transform, if only to easily find spots in our own code that won't compile because of it. I'd like to find and change such code for stylistic(?) reasons in any case.

(By inspection, there currently appears to be one such rethrows func in our code base, which I'll be changing now...)

1 Like

It's hard to quantify without doing it. With the above, we'll have to decide what the rethrown function type is going to be for a closure that has that do...catch... which is the errorUnion of the various try sites. I suppose we could recognize specific is and as constructs in the catch blocks to remove errors from the union as we go through.

FWIW, you don't need the is XXX in these cases, because the implicit error will have the appropriate concrete type.

Doug

13 Likes

I remain partially unconvinced, but you make a good point. In any case this sounds like something orthogonal to the proposal - as a pre-existing concern - and so best handled separately, e.g. in a future SEP.

I apologise, I had forgotten about this, from the detailed discussion of rethrows in the prior pitch review thread. Indeed @xwu covered that specific possibility. Thanks for reiterating that important point.

You're right that it does make the typed throw version closer to equivalent than I was thinking, at least for most [likely] uses. However, you might also recall from that prior thread that there are valid, realistic cases where the intended semantics - of the existing rethrows - are not enforceable with just typed throws (as opposed to typed rethrows), such as this example by @michelf. Not common use-cases, for sure.

I assume it's not fruitful to duplicate the discussion on this here - I somewhat summarised my thoughts previously, and @anandabits helped refine my point - and I trust that the proposal's reviewers will incorporate that earlier discussion & feedback.

I certainly acknowledge that rethrows has some enforcement issues (although I'm also not stressed by them, because unlike e.g. memory or concurrency safeguards, the holes in rethrows are much less likely to lead to severe effects like memory corruption or invalid program states).

I want to agree (on the procedure and timing - I still think rethrows has merit and is worth preserving).

My hesitance to defer the rethrows question is because until rethrows is properly resolved (one way or another), we'll have an unfortunate time period in which users are forced to choose between rethrows and typed errors. It would, arguably, be the more conservative option to support typed rethrows as part of this SEP as it'll mean users have to make fewer tough decisions and fewer [undesirable] semantic changes to their code, in adopting typed throws. And that still doesn't preclude then reviewing rethrows and potentially removing it (which is the source-breaking aspect that, as you note, is a much bigger deal).

I also fear that people will predominately choose typed throws over rethrows if forced to, and this will be misconstrued as a "popular vote" against the unique benefits of rethrows.

But - lest we forget or I appear under-appreciative - I do want to thank you once more for the incredibly thorough thinking and work done on this proposal, especially as you say with an eye towards avoiding source- and binary-incompatible changes. I'm really impressed by some of the subtleties you identified and found elegant workarounds for (for Swift 5 mode). As much as I've heartily debated some aspects of the proposal, such as rethrows, I find very little overall that I can even nit-pick about.

Exhaustivity checking in catch

I appreciate the extra details & discussion on why exhaustivity checking for catch is implementationally difficult. It certainly convinces me that it's reasonable to defer any further improvements there to a future SEP, as much as I'll miss the functionality in the meantime.

I suggest, though, including a little of that "why not" detail in the SEP itself, for future readers (who are more likely to find the SEP than this forum thread). Both to better explain the rationale and also for the interesting, educational glimpse into how the type checker and exhaustivity checking do [and don't] interact today. Plus it will help clarify that there's no dispute about the desire for the checking, but rather that we just don't know how to implement it efficiently enough [yet].

(and I use "we" in the most self-aggrandising sense here, because I haven't the faintest clue how to solve that thorny performance issue :laughing:)

1 Like

I think the question to ask is if we had started with generic typed throws from the beginning, would we subsequently feel the need to introduce a typed equivalent of today's rethrows? I suspect the answer is "probably not".

10 Likes

That hypothetical was discussed in the earlier thread as well, and no real conclusion reached.

In any case, there's a lot of existing features - that we appreciate and take for granted - which quite possibly wouldn't survive the Evolution process today, whether because of technical barriers (e.g. frozen ABI limitations) or too much controversy or bike-shedding or whatever.

Overall I am in favour of this pitch, the inter-operability between throws and Result, Task etc sounds great to me.

However one thing I don't see addressed is inter-operability between throws and KeyPath. Currently, throwing properties can't be represented by KeyPath (same for async as well). This is one advantage Result type has over throws, and the general reason I favour using typed equivalents like Result (or Future) instead of using keywords like throws (or async).

2 Likes

Personally, I think that Swift would be better off in the long term if typed throws replaced rethrows, because we'd have a smaller language built out of more composable parts. Based on the use cases you linked above, it's possible that can't happen. Where the proposal is today---leaving rethrows as is---definitely does tip the scales slightly in favor of this end state where rethrows is gone, but without committing us to that course right now. If I'm right and rethrows becomes unused, we can deprecate it at some future point once typed throws has rippled through the ecosystem. If I'm wrong, we can go back and extend it with typed-rethrowing capabilities.

Thank you!

Yes, that's a good idea. I've struggled to find a way to concisely describe the issue.

Right, effects like async and throws in key paths is a clear deficit. I don't think it should be addressed in this proposal, because any improvement there should handle both async and throws, and is very likely to need to introduce more types to the key-path hierarchy. I don't have any good solutions here right now.

Doug

8 Likes

A quick read-through.

I’m +1 to the overall proposal and functionality will be a great addition to the language that I’ve been missing many times.

My main issue is with the syntax. It’s new (except for the unowned(unsafe) example given) and makes function types more difficult to parse (for us mortals) due to the extra parenthesis (e.g. is it an argument or maybe a tuple return type?).

Has a prefix along the lines of @escaping been considered (it isn’t in the syntax section of the proposal)?
Maybe something like @throwing(ErrorType)?
To me, I think a syntax like that would make the language look more cohesive.

Yeah, I get where you're coming from now. While I'm still not convinced rethrows is doomed, you have convinced me that it's reasonable to proceed as you propose.

So, I think the proposal looks good to go!

:+1:

result is not the goto replacement for throws in imperative languages

Using explicit errors with Result has major implications for a code base. Because the exception handling mechanism ("goto catch") is not built into the language (like throws ), you need to do that on your own, mixing the exception handling mechanism with domain logic.

This paragraph is very confusing

Which imperative languages is it not built into? Is this about c/c++ interop?

If the imperative language is swift, then it is built in, it's called catch, and goto catch: would not work anyway.

I propose to rewrite this for clarity

If this is about interop, show an example of the other language.

If it's a (valid) critique of Result as a replacement for swift throws/try/catch, then don't mention goto, since switch is the obvious choice:

switch functionReturningResult() {
  case .success(let result):
    …
  case .failure(let error):
     // respond

"goto catch" is figurative, not literal. It's expressing that Swift's exception-handling machinery provides - like it does in most languages with exceptions - a way to effectively jump directly from the site of an error to the intended error handler, without any manual labour. All the code & functions in-between are automatically unwound and gracefully terminated for you, with (in the common case) no effort on your part (the uncommon case being where you might need to use defer or similar constructs to finish up cleanly).

As opposed to e.g. Result where you - the coder - have to manually write the code to detect the error and connect that to whatever error handler is appropriate (which might be many levels up the stack, requiring this boilerplate be repeated over and over again to achieve the desired result).

3 Likes

I've been experimenting with adopting typed throws in my Alamofire websocket work. You can see the relevant changes on the feature/websocket-request-typed-throws branch. As you can see it's a fairly straightforward conversion. However, it seems the source compatibility concerns render it less useful in Swift 5 mode, as you can't actually catch your typed errors. Adding the suggested as! Serializer.Failure fixit results in a compiler crash. So it may still be useful, just requiring that dynamic cast in Swift 5 mode.

3 Likes

Note that the only way to write an exhaustive do...catch statement is to have an unconditional catch block. The dynamic checking provided by is or as patterns in the catch block cannot be used to make a catch exhaustive, even if the type specified is the same as the type thrown from the body of the do

I'm a bit concerned about this detail from a slightly different perspective than the above comments. It seems like this is essentially requiring a code author to rely on type inference for the error value in a catch block. In general, where Swift has type inference, it also has ways to explicitly provide the type information (e.g. property declarations, closure parameters, as Foo applied to method calls). This can be important when trying to troubleshoot why type checking has gone awry, for clarifying and formalizing the type that you expect, and when using type information to select an overload (though this last one wouldn't apply for catch blocks).

Just as in those existing cases, I think it would be equally useful to let authors state the type of the error they expect, and have the compiler tell them if that expectation is incorrect. I think catch is CatError and catch let myError as CatError are probably the best syntax for this. I'm extremely reluctant to propose new syntax, but if those are too closely linked with pattern matching, then maybe something more binding related would be acceptable? This doesn't really look right to me, but has the needed parts:

do {
    try callCats()
} catch let myError: CatError { ... }
11 Likes

On a related note, this statement from the proposal seems odd to me. It's either over specified or making a distinction I've never seen about Swift before.

The semantics specified here are not fully source compatible with existing Swift code. A do...catch block that contains throw statements of a single concrete type (and no other throwing sites) might depend on the error being caught as any Error .

Is there actually a difference in behavior between

do {
  throw CatError.asleep
}

and

do {
  try callCats()
}

?

Should the sentence be "A do...catch blocks that contains throwing statements producing a single concrete type (and no other throwing sites) might depend on the error being caught as any Error." (emphasis added)? It seems like the "might depend on" part is equally applicable to all sources of errors but the "and no other throwing sites" can be read as "and no other throws". And as I stated, my interpretation does match what I'm seeing in proposal toolchain. (By the way, how do I enable FullTypedThrows? I tried all the flag combos I could think of and nothing let me catch specific errors.)

1 Like

Are you using the correct swift toolchain which has the TypedThrows feature?

+1 for me.


@Douglas_Gregor shoudn't the stdlib rather adopt a new typed throwing Result.value property to align with other APIs like Task.value etc.? I think get() should be left as is and instead we should revive this pitch, but this time around with typed throws.

1 Like

Yes, the toolchain linked in the first post. I can get the TypedThrows feature working just fine, it's just the FullTypedThrows, that enables all of the inference rules, that I can't.