Typed throw functions

I don't quite get, why 'normal' (meaning not resilient) libraries should not use typed throws. When I write such a library, I could have an enum with error cases, which gets thrown, and if I introduce a new case, I surely want the library user's code to not compile anymore if they aren't handling all the cases in their catch. We also don't require the user of a non-frozen enum to write an @unknown default in every switch (But of course I would be pleased, if we had something similar for catch that can optionally be used).

If I, as an API designer, know that my enum will change often, I still have the choice to not use typed errors for it, as that is still the default, when I don't specify a throws type.

3 Likes

I'd like to remind there there's a whole document regarding design details that may answer some questions that are raised here and some others that still need some attention.
The major drawback of this getting into a formal proposal is the need of a working implementation, which is a big change into the compiler system without having why guarantee that in the end it will get through the review process.
I'd like to explore how this could be implemented as a mock and later on as a whole, or just have enough devs to implement this, as I am not able to do it by myself clearly.

One exception to the single-type rule might be an API such as closeAfter(_:).

 extension FileDescriptor {
   public func closeAfter<R>(
     _ body: () throws -> R
-  ) throws -> R {
+  ) throws Errno rethrows -> R
 }

If body throws either no error or a typed error, should catching only one or two types be exhaustive?

Is that valid with the current proposal? We either thought of giving rethrows a type or making it that it erases the inner typed throwing type of defined.

Does the current proposal require a generic parameter for each rethrown error type? It would be simpler if typed rethrows didn't require any changes to existing APIs.

For my throws Errno rethrows suggestion, the rethrown error type would be implicit. But I didn't consider the case where an API takes multiple (trailing) closures.

rethrows behavior is one of the hardest things to align with the proposal, in the rethrows section, if you wanted you use an inner typed throws closure and we don't want to change current behavior nor syntax, it would be always erased to a plain Error. Maybe in further evolutions rethrows could be even deleted (as were commented in the past) or add some kind of typed syntax to it, but I think it gets out of scope. Suggestions are welcome though, the proposal is not a monolith and is open to changes of course!

I suggest to give a deep read to the rethrows section and see if we can come up with better ideas or options for it (while I'd love to get rid of it, sincerely)

If your main concern is the performance impact of allocating when converting an Errno to Error, have you looked into solving that dynamically? After all, even with typed errors, your clients may still need to throw Error. If you can efficiently represent an Errno in Error, that seems like it would have broad benefits for other users (assuming it isn't actually hard-coded to Errno, which I don't think we'd accept).

That is not my concern. It was an issue before the workaround I posted. My concern is that there are situations where, as a library developer, I want to make an API that can throw errors which can be exhaustively surfaced to the user. There is no way to express this in the language, so it is expressed in documentation and conventions. Since my libraries (the stdlib and System) are source and binary stable, this convention should not be violated. But, it is unenforced and make users code more complicated and less clear.

(Random tidbit: We also have a retryOnInterrupt: Bool = true parameter on every syscall that can EINTR/EAGAIN. This is the main case where you really really want to avoid a box. If a syscall actually hits kernel space, the file system, or a device before failing, then the cost of a box is likely inconsequential).

Within the performance discussion (which is not my main argument for typed throws), this is fine and expected for all of my use cases. If I exhaustively surface the cause of failure (e.g. a Unicode decoding error, or Errno), clients can handle the error cases concretely at the call site. If they propagate the error up the stack, where it can mix with other errors from e.g. a do {} block, then I don't foresee boxing being the most relevant performance concern. When an error goes from typed/exhaustive to untyped, and is propagated upwards, I expect there to be much more going on in recovering, retrying, or cancelling operations than the cost of an error box.

An in-line representation for Errors might be interesting, but should be evaluated considering the impact on the whole system. E.g. measuring potential memory savings/effects across an entire OS or server instance. I would hesitate to add complexity, especially if it resulted in code size (if small check is emitted) or runtime complexity (if it's inside the runtime), before doing that kind of analysis.

This really seems like something you'd want to investigate post typed-throws. IIUC, if the Error is still untyped, there then always be a need to materialize something (a witness, metatype, or whatever).

I disagree that the performance investigation is less important and should wait until after the addition of typed throws. Embedding errors into Error will always be important to make efficient, even with implausibly broad adoption of typed throws.

4 Likes

I did not mean to imply the performance investigation is less important, but it seems to me that it naturally follows after typed throws. Typed throws can be used to communicate exhaustive failure reasons directly to the call site, where they can be exhaustively handled and recovered from. IIUC, there will be tradeoffs involved in small-error forms and an investigation would help us choose between them. My concern is that a system-wide investigation, prior to typed throws, would produce skewed results for errors are both exhaustive and ephemeral (meaning they are directly handled by the call site and not propagated upwards as untyped errors).

Either way, this performance discussion seems orthogonal to the proposal at hand. Unless I missed something up-thread (I did not read every post), I haven't this change being motivated for performance reasons. I did mention how System chose throws over Result as the Swiftier pattern and how we worked around performance issues, but I hope I've been clear that error boxing is not my main motivation.

If typed throws that are fully handled at the call site still ended up inside heap-allocated boxes, while that would be odd, it wouldn't defeat the purpose of typed throws for library development.

1 Like

Still, this would need help to be implemented if a formal proposal is going to be presented, as I intend to do, but cannot do it by myself or without any kind of support of the community. This considering that the implementation is needed to make the proposal and make it through a review process, prior to a formal implementation. Correct me if I'm wrong with this.

Sorry for the ping @John_McCall don't know every detail about review process. :sweat_smile: