Typed throw functions

For me using map calls remember me to closure hell. I prefer (personally) a more linear programming. When you start nesting Result calls using or value unwrapping or map makes it even harder to handle. Also this approach seems to just avoid error handling, which is the whole purpose of this discussion, the visibility of the word and how are being handled properly instead of generically across the language.

Thanks for the answer!

2 Likes

This two points align with typed throws.

  • Typed throws won't let you throw a type that died not match the visibility of the function which throws it.
  • Errors being changed should always be as breaking as API one's, as they are part of the exposed API to the client, the client will have to update it as they update their functions.

And lastly, this gives error handling visibility, now it is opaque and obscure, like all the foundation APIs that throw "something" when in their objective c signature clearly is a NSError. With Swift we've killed typed errors.

NSError is not meaningfully "more typed" than Error.

11 Likes

Code, domain and user info with (if the developer wants) can give more context.
What I tried to say there is that there was a type that represented an error, which you could subclass or use your custom one in order to handle errors, but you knew beforehand what was going to come back, because it was in the function signature, not pretty for Swift that's for sure, but returning that type to the scene is maybe worth. That's my point for the comparison with NSError.

1 Like

I was never arguing against typed throws. There are plenty of parallels with Result, and that is why I brought it up. I see typed throws as basically sugaring Result to look more like the older throwing syntax. I guess my opinion is this: Typing is valuable at times and not at others, so both possibilities must co‐exist. Giving them both a more similar syntax would be less confusing (and throws is more concise), and that is probably good. But it wouldn’t add any new capabilities over using Result, so I don’t see it as a very high priority. I’m neither pushing for it nor objecting to it.

IMO, the domain has been replaced with the type of error, which you can extract much more easily now with new catch syntax SE-0276. You still need to handle unknown domain of course, which falls into unconstrained catch clause, as usual. I don't think NSError is any more expressive/informative than Error.

8 Likes

What I tried to say is that, from this:

- (NSArray<NSString *> *)contentsOfDirectoryAtPath:(NSString *)path 
   error:(NSError * _Nullable *)error;

Where we got the error implicitly in the call so we could know the type (doesn't matter to me now if it was more expressive than Error or not, that's not the point, neither I'm suggesting to go back to ObjC).

To this

func contentsOfDirectory(atPath path: String) throws -> [String]

Where we lost totally the context of what is being thrown. As I agree with you that multipattern catch might help, It does not help in any way if you don't know what to catch in first place, which makes you go to the documentation and find what is being thrown at each moment by each function, whereas

func contentsOfDirectory(atPath path: String) throws NSError -> [String]

Is self-documented (a thing that I always try to achieve when I write down code. And which is not available for me right now in Swift unless I mark in the documentation of the method which Error is throwing.

I expect to have explained myself enough with from the last 2 posts that obviously didn't make my point sufficiently clear.

1 Like

I don't know if it was already mentioned upthread or not, but one of the reasons I want typed throws is exactly this:

This would also beautifully fit with throwing accessors:

4 Likes

This'll be my last post on this particular topic since I don't think it's very relevant to the discussion at hand.

It may sound good in theory, but the example is not exactly convincing. I checked the documentation and still can't say which of the error I'm fishing for. NSError is ubiquitous. It might as well throw Error and that wouldn't make any difference. Cases like this are precisely appropriate for untyped errors.

I'll give the idea the benefit of the doubt, but Foundation is not a good example given that it only uses NSError and most functions don't even bother documenting which error it is throwing.


I think a stronger point regarding its usage (so excluding things like type system unification, etc.) lies in where typed and untyped errors differ. Typed errors do enjoy exhaustivity check, which would fill in the gap between Optional (exactly one error) vs throws (unlimited error). It's probably important anyway to arrive at a guideline as to when the typed error should be used instead of something else.

1 Like

I really appreciate all of your contributions to the topic and appreciate the caveats and strong points. Let more people participate in the topic (if they want) and then we can make decisions. I'm here just to move the topic but not to make it mandatory :smile:
Being true that NSError was the Error that we have in Swift where anything can be behind the scenes. I feel that adding the type breaks that uncertainty against catching Error but catching ConcreteError and make a better error handling. This is unlikely to be implemented in Foundation (farther than generating an automatic conversion from ObjC NSError functions to throws NSError) so the "old" Foundation probably will be there as a monolith.
New implementations in Swift are another topic, because they now start to throw Error descendants, which makes this type of annotation more useful.

I agree, I have someone in my team that is making exactly the same argument - they eschew throws entirely in favor of Result because you can type the error correctly.

Type-erased errors are very important for large scale app frameworks, but that is not the only kind of code people build in Swift. Pushing people to use Result instead of throws is a failure of the design, not something to be celebrated. The throws design in Swift is carefully considered and a huge progression vs manual error propagation, we shouldn't hobble it like this.

I (still) think that lack of typed errors is a huge omission from Swift's error handling approach.

-Chris

33 Likes

In my team we're pushed towards a Result API because we don't have any other tool to type our errors thrown from different architectural layers. For me, opinion, Result was added as a lack on the error handling system which the multi catch pattern nothing has to do with. I end up always using get() to have a throwing method and avoid map closure hells.

That's why I'm convinced that there's something in the language that feels not finished.

2 Likes

Sure - so my issue with typed throws isn't so much with the specific feature itself. Error is just a marker protocol, and by itself is no more useful than Any. You wouldn't write an API taking Any parameters and returning Any results, so it makes writing code (especially code which actually handles errors) more awkward to write as you have to claw back that type information via dynamic casts and handle failure cases which you know will never happen (especially in a single module).

That said, I think that introducing typed throws right now would lead to an overall worse error model than we currently have. There are a couple of reasons for this, mostly revolving around enums. Conceptually, enums are a really great fit for modelling errors, so it isn't surprising that those flaws really get exposed through throwing functions. Also, I agree with @Lantua that the major motivating use-case for typed throws is exhaustive catching, and that only works on enums anyway. So enums and typed throws have a very strong link.

The biggest issue is that you cannot add new cases to public enums. This is something of a flaw in the language - and has been a topic of much discussion. Essentially, it means that we shouldn't really be allowing exhaustive switch or catch on enums from other modules in the general case anyway - you should always have to handle @unknown default cases, so the enum has freedom to grow. We currently have a quirk in the language so if the enum's declaring module was compiled with library-evolution, it will actually be extensible and cannot be exhaustively switched/caught unless you opt-in to that. The thing is - it's also a massive issue for source stability, not just binary compatibility. Unfortunately, there isn't even a way for enums in regular modules to opt-in to that very important flexibility (that's what the proposal I linked to aims to address).

So why is this a problem? Well, let's take your example:

public enum AuthenticationError: Error {
    case invalidCredentials
    case unknownReason
}

public func authenticate(with username: String, and password: String) throws AuthenticationError

Let's imagine this is part of a library - not even an ABI-stable library, it can even be shipped as source. Now, you've had the foresight to add an unknownReason catch-all clause, which is good. Lots of developers won't think of that - especially if they've never run in to the issue themselves or frequent these forums to have heard about it. But it won't save you ;)

So they ship the above code in v1.0 - but soon after, they notice some suspect behaviour, and decide to rate-limit the authentication requests. Now, this company has really poor frontend caches, so they recommend (but don't require) that Apps handle this specific error and enforce the limit on the client side. Even with an unknownReason case though, we still don't have any ability to add errors to the function, other than bumping it up to v2.0. This creates yet further headaches with package dependency resolution, as all dependencies must be compatible with the same major version of the library.

So you can see how enums make this feature impossible to use without limiting how your function's implementation can evolve, even if it isn't @inlinable. That's why I think it's important to fix enums before we give them even more significance by exposing them in this way.


So that's an overview of the problem. Will it be an actual issue for developers in the real world? I think it will. Just looking at this thread:

It sounds very much like people think that typed throws is something that they "should" do. Of course, it would eliminate some boilerplate, but in many cases, it could inadvertently expose some non-obvious pitfalls in the language's library evolution story (both for ABI and source stability).

In general, I agree most with the sentiments expressed by @John_McCall:

P.S. For @Chris_Lattner3: Are those functions public and are the errors enum types? If so, is this person aware of the evolution constraints?

Let me be clear, I think the errors that would benefit from exhaustive check (and hence typed error) would:

  • Be exhaustible (mostly enum),
    • Don’t have a case that simply wraps an inexhaustible error,
  • Don’t have unknownError and the likes–it’d lean more toward untyped throws,
  • Expect each error case to be handled differently–it could use Optional otherwise.

Personally, I haven’t seen many cases (if at all) that would fit this criteria.

2 Likes

I think it is developer responsibility to bump to v2.0 or not if an Error case is added. Many of them won't, because it is an additive change and not a breaking change hence the bump would be minor in most cases. If you add a case you have the exhaustive switch, which make the change additive and just have to update a case. At any moment the developer made the enum @frozen so it is able to add as many cases as he wants. If you're making just a generic catch, you won't care about new additions, you're just behaving as you'd do now: "I know there was an error, let's log/print/prompt to the user the localizedDescription", for example.
I don't see the issue you describe as an issue or as something the community cannot learn from, as it does from every additive change we make to Swift (and we don't make major versions for a while :stuck_out_tongue:)

1 Like

Though they are strongly linked, exhaustive catching isn't the only use-case for typed errors. Boilerplate elimination is a reasonable ask, too. In order to decouple those things, we need to fix enum evolution.

The enum improvements should be high-priority fixes anyway IMHO, regardless of our plans for typed errors.

1 Like

Which boilerplate are you thinking about?

Generally removing downcasts and the corresponding failure branches, by virtue of knowing the exact type of error. That's plain type-safety, and there are optimisation opportunities from knowing that. Whether or not you can exhaustively handle every value of that type is (or should be) a separate question.

Removing boilerplate is not going to be the #1 most amazing feature, guaranteed to knock your socks off, but it's a reasonable ask.

1 Like

I see. You mean something like this?

// bad 1
do { try untypedThrow() }
catch let error as TestError {
  // error is `TestError`
} catch { /* dead code */ }

// bad 2
do { try untypedThrow() }
catch {
  let error = error as! TestError
  // error is `TestError`
}

// good
do { try typedThrow() }
catch {
  // error is `TestError`
}
3 Likes

Quoting myself in the past: