SE-0413: Typed throws

That was the behaviour in its very first iteration, in Swift 2. In Swift 3, rethrows was revised ("loosened") to mean what it does now: that it can throw [any type it wants] only if & when one of its arguments throws, in order to specifically enable wrapping of lower-level errors (perhaps since generic Errors are not extensible nor chainable, but it's common to want to attach additional information to an exception record as it propagates up through multiple layers of API).

For further details, see this series of archeological posts from the earlier pitch review thread.

Also, welcome back! Or are you just here to nab ideas for your new language, like @dabrahams? :grin:

7 Likes

Can it be? Isn't it baked into the ABI due to functions like Sequence.map?

AFAIR, rethrows functions use the same mangling as throws. When such a function is invoked with a non-throwing closure, an unreachable "catch" branch will be emitted on the callsite.

1 Like

ABI is more than mangling, but yes---the ABI of a rethrows function is identical to that of a throws function. If you're curious, you can see what I did to swap in a typed-throws map in an ABI-compatible manner.

Thanks for trying out the toolchain! FullTypedThrows can be enabled as an "upcoming" feature (in addition to enabling TypedThrows as an experimental feature, and will change inference for the caught errors in, e.g.,

do {
  if something { throw HomeworkError.dogAteIt }
} catch {
  // error will have type HomeworkError when FullTypedThrows is enabled
}

However, as noted, we aren't doing type inference for closure thrown result types in the toolchain.

Right. You can actually exploit this fact to get "typed throw" behavior without FullTypedThrows being enabled, by adding this...

func typedThrow<E: Error>(_ error: E) throws(E) {
  throw error
}

and replacing a throw within a do..catch like this:

try typedThrow(HomeworkError.dogAteIt)

Obviously not as nice as FullTypedThrow behavior, but it's more incremental.

Doug

4 Likes

Yeah, I tried a bunch of permutations of FullTypedThrows as upcoming or experimental features, but it turns out the behavior I'm seeing is probably just a bug, so I filed [Typed Throws]: Error thrown from try not inferred for catch · Issue #69985 · apple/swift · GitHub. Perhaps it has difficulty with the fact the typed error comes from a protocol requirement.

1 Like

I'd expect your code to work with -enable-experimental-feature TypedThrows -enable-upcoming-feature FullTypedThrows, so this is likely a bug in the toolchain. I'll take a look, thanks!

Doug

2 Likes

Which part would require FullTypedThrows? From my current understanding, neither of the inference exceptions apply here. It's not a direct throw nor is it attempting to throw out of a closure.

You are right that it's not related to FullTypedThrows; it's related to the do...catch block being within a closure, which is a subset of the closure inference logic, and the toolchain isn't handling such cases yet.

Doug

2 Likes

Ah right, thanks for the reminder!

I don't have time to follow Swift development generally, but I'm a huge fan of it and the community of course, and typed throws is a design point I've been in favor of for a long time.

In the case of Mojo🔥, its generics system is just coming up now and yes I'm definitely interested in what Swift and other languages are doing. In this case, I'm particularly happy with how the Swift team makes it possible to abstract over the throws effect here. This has been a persistent problem in Swift for some time, and I'm interested in making sure Mojo gets this right from the beginning, and also extends it to async (our other effect).

-Chris

21 Likes

For subtyping purposes, Never is assumed to be a subtype of all error types.

Can we please finally generalize Never as the true bottom type? This feature is a great motivation, and making it a special case feels really unfortunate.

I just want to finally be able to use fatalErrror in the nil-coalescing operator :smile: (yes I know I can use a custom operator or macro, but it would be great for it to be standard).

11 Likes

I think it is a good addition, but imo it is not so nice to introduce yet another meaning for parenthesis — without even mentioning an established way for type annotations (throws: E) in the alternatives section.

If you think of it as taking an additional parameter, it has the same meaning as most of the other paren usage in the language.

2 Likes

Under the hood functions with untyped throws actually have an error parameter: a pointer to the existential box to put an error value into if it were thrown.

pass a pointer argument that doesn't interfere with the normal argument sequence. The caller would initialize the memory to a zero value. If the callee is a throwing function, it would be expected to write the error value into this argument

1 Like

This is getting off-topic, but it's not really a pointer to the error box. With optimal calling convention support, it's more like we return a pointer to the box in an extra return-value register. Without that support, we pass a pointer to a variable into which to write the pointer to the box.

The typed throws calling convention, in the pessimal case, degrades to something more like what you're saying, except that there's no box: the caller passes a pointer into which the callee should write the error (plus some other mechanics for determining whether an error was thrown, some of which haven't yet been done the way we want).

2 Likes

What is your evaluation of the proposal?

I love it! Big +1 from me. This was the feature I was missing most in Swift. And the way it is executed makes a lot of sense and fits right into the existing feature set and APIs.

Is the problem being addressed significant enough to warrant a change to Swift?

Yes, definitely! It will entirely change the way I do error handling in my apps, mostly making it easier and clearer. This feature will directly correlate with how useful error messages will be to my users. Also, some well-chosen functions in my open-source libraries might use this, too.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

No in-depth study of the implementation, but I have thought about error handling in Swift in depth. I even prepared an error-handling framework for app developers based on the Result type to work around the current lack of typed throws in the language (I didn't release it yet because I saw this proposal coming which will have a big impact on that framework). I've read the full proposal twice (first an earlier version of it) – note though that I'm no compiler expert and do not understand the Swift grammar part fully.

Does this proposal fit well with the feel and direction of Swift?

The Swift website states Swift is "modern, safe, and a joy to write". This proposal makes Swift even more modern, safe, and a joy to write. So: Yes! Less guessing of potential error types for APIs where they can be made clear. This means basically all of my own error types I create within my apps can now be clearly communicated both to the compiler and to the users of those APIs. This will reduce a lot of boilerplate I currently have in my code and make my code safer while reducing cognitive load by offloading that to the compiler, improving safety & joy. (I had already stated why untyped throws are not safe here.) And the way it still supports the untyped throws feels very modern, giving the developers the choice. Another useful tool in their belt.

6 Likes

An alternative might be to specify the type not in the catch clause's pattern, but as an effect on the do block instead:

do throws(CatError) {
    try callCats()
} catch let myError { ... }

You might even be able to apply the typed throws effect to a catch-less do statement if you simply want to state the allowable error type explicitly.

15 Likes

This is a really cool idea. This would also give typing context for the throw statements within the do...catch, i.e.,

do throws(CatError) {
  if isDaytime && foodBowl.isEmpty {
    throw .sleeps
  }
  // ...
} catch {
  // ...
}

Doug

12 Likes

Can you clarify why that's better than the existing syntax? It seems both unintuitive¹ to me and merely functionally equivalent.


¹ It reads "do throw this error", as if you're insisting the clause throw, almost like you're trying to phrase a guard block i.e. do throw this error otherwise run this exceptional code; the exact opposite of what's actually happening.

1 Like

Better than what existing syntax? There is no existing syntax for the semantics that Nate is requesting.

1 Like

That's what is unclear to me, then - what is this doing that's novel / distinct?