Typed throw functions

At this point, is anyone interested in helping me writing a pitch and in parallel in some implementation work (basic semantics to start of, AST additions, etc.).

Thank you!

Keep in mind that people can already do the "bad thing" that you're worried about here: they can vend a public API implemented with Result and a typed error. Adding this to throws doesn't introduce a new evolution problem, it makes the language more consistent.

Beyond that, there is a lot of code in the world that doesn't care about long term evolution, because it isn't a public API. A very good (and pretty standard) design is to maintain a minimal public API (where API evolution is critical) but then have a large / complex internal implementation details within the module.

Also, a lot of people build apps and other leaf things that are not meant to be shared as API. Evolution considerations are quite meaningless here. There are also projects with different philosophies w.r.t. their APIs - e.g. LLVM and Swift intentionally don't maintain stable C++ APIs, despite having downstream users.

My point here is that "long term evolution" is important for large scale app frameworks and other important classes of libraries, but this is only one of the sorts of thing that people build with Swift. We are underserving the other cases, pushing them to use Result with typed errors.

-Chris

21 Likes

No arguments there - they certainly can do that. I'm considering this from the perspective of "what if there was a PR for typed throws tomorrow?", and whether we want to encourage developers to commit to particular Error types now, while enums are still so broken. Again, this is all about enums - but they are closely related to this feature (especially when it comes to exhaustive catching), so it's important to consider them both.

No argument here, either - however, my understanding is that one of Swift's goals is indeed to support both forwards and backwards compatibility for libraries, and to make it explicit whenever you give up future flexibility for performance or other reasons.

Of course I'm not objecting to propagating error type information whatsoever (and as you say, you can do with Result). That's a decision that developers can make on a case-by-case basis: the problem is that as things stand, enums violate the contract mentioned above. I think I am correct in saying that they are the only types in the language for whom adding information (i.e. data members/cases) is a source-breaking change. It's an ordering issue: we should fix that language inconsistency before we think about tightening contracts for error types, because the 2 features are so closely related. Doing it the other way around is worse.

Yes, I agree with you that typed throws is a feature that could be used incorrectly w.r.t. API design (this is typical of almost all features in Swift), and I agree with you that improvements to enum would also be important.

Neither concern seems like it should slow down progress on typed throws though.

3 Likes

An authentication can fail for two basic reasons: either you have the wrong credentials, or there was a mechanical failure in the attempt, like a network failure. These two things usually need to be handled/reported differently. There is no reason to make them the same type of error except a misguided belief that having a single error type is somehow better.

This is exactly the sort of thing that makes me not want to support typed throws: a well-meaning programmer corners themselves into a bad design because they instinctually believe that the abstract taxonomy they're developing is useful.

11 Likes

And that does not apply to Result?

It does apply to Result. Most people should use Error as the error type for their results. I regret that Result doesn't push this harder.

5 Likes

I have the sense that more people still use enums that conform to Error with Result just as if we'd let them make it with throws

1 Like

I can't see a connection between those two. What you saying is lack of anonymous "union" types in Swift, how does that relates to typed throw? We are forced to use ad-hoc types everywhere when we need such this-or-this behaviour (I've wrote hundreds, I don't like it, but it's much worse without them).

Also, in my opinion and experience (although small), you can't force developers to make good design choices at such high level by removing or not adding features to language, they will always find a way to make it even worse than you intended. Creativity! :slight_smile:

That's not what I'm saying at all. A union — whether explicit or anonymous — says that the error is one of a few defined things. That's not what you want to express here, because an authentication API can fail for all sorts of reasons that authentication doesn't have any special knowledge about. As the provider of this API, what you want to express is "this can fail for all sorts of reasons, and one of them in particular is that the credentials might be wrong". As the user of this API, you want to check specifically for the case that the credentials are wrong and then handle all the other cases uniformly. Neither of those is made easier by the authentication API adding extra structure to all the other cases.

3 Likes

Then for Authentication purposes you can just use the current throws, but there are infinity (literally talking) of other use cases. Even more, if we talk about Authentication with its multiple domain of errors were could've been talking of throwing generic errors, which would flex that specific use case that, currently I'm doing with 2 layers that throw two kind of different errors and a result layer to merge it into just one, thing that it could be done too with this proposal.

Swift I think added Result to fill the gap of Futures with the arrival of Combine, letting the Official Swift Guide in the section of Error handling totally (IMO) unfinished.

Well, but the thing is, the only sort of system that a tightly-limited typed throws actually works for is something inherently at the very bottom of the stack, like a POSIX wrapping library that wants to throw a POSIX error code. And even then, on the client side you almost never want to take advantage of that — most of the time, you want to handle a couple specific error cases and then let the rest of them propagate normally.

The only strong arguments I've ever seen for typed throws are (1) to make it easier to verify that you do restructure errors in situations where that's actually important and (2) to enable a micro-optimization where errors don't have to be boxed, which is something that matters in very narrow circumstances.

Result's importance in asynchronous programming is really orthogonal to whether or not it uses typed errors, and we're planning to address that directly.

3 Likes

TBF, the boilerplate occurs in the first place due to the API author using typed error, regardless of whether it is appropriate.

I still wish to see more concrete examples where typed-error is appropriate. Combine is a relatively good example so far, followed by AuthenticationError, though I agree with @John_McCall that it'd better be untyped. Foundation is a non-starter in this regard.

I suppose there are some scenarios where you truly have an enum with a fixed number of failure reasons and all of those failure reasons are in fact related to whatever action your API is performing. However, I haven’t seen them a lot in practice in my experience.

I have seen quite a lot of error enum abstractions with all sorts of cases in it to handle various different scenarios, many of which might not actually be related to the action being performed (John’s example about authentication and network failures above is one of them). I’ve also seen quite a lot of error enums (and in fact, I’ve written some myself in the past!) where the enum has an unknown or genericError case as a ā€œcatch-allā€ or even something like unknown(error: Error) case, which I don’t think will benefit from typed throws (or even Result for that matter if you’re using a typed error).

I’ve realised that this might not be good design. I suppose typed throws might provide more opportunities and contexts where such design can flourish, but maybe some of the proposed benefits of this feature could outweigh that? I’m not sure...

(By the way, I would love to see more real-world examples where typed throws would be really beneficial. I know not everyone is writing a public API and etc, so do you have any good examples from other sources, like an app? One place where I’ve used an error enum is text validation, where I have a bunch of fixed failure reasons like the text being empty or not matching a given validation rule). I currently use a Result but do you think typed throws would be better? If so, why?)

But does it makes sense on searching which domains does a typed throw fit or not? Wouldn't be that work of the developer if has the tool to do it? It feels like asking me what would I build with a plank, but without the plank nor wood knowledge :sweat_smile:

In my case, we throw all the possible networking errors into 6 cases, that are raised to a layer above it. We do make decisions in each one of them: logging, UI changes, retrying, etc. I'd like to know that function throws only a type but not NSURLSessionWhichever, that going to the docs, there's a limited amount of codes that could be wrapped in a type where everyone can find them.

Anyway you're taking my Authentication example to the end of the world and it was just the first thing it crossed my mind because I had an auth file opened in my Xcode ATM.

2 Likes

Won't you use the same approach using Result?

My point here is that we do have to rely on a enum type to handle success or error when we have since day 0 a built-in error handling that aligns with other languages butt lacks of an optional typing (we do like type things in Swift) that you can use if you like it or leave it if not, up to the developer, but at least we can provide the full set of tools and of the error handling system of a (what I think) should be a strong typed language.

My experience has been that these ā€œcatch-allā€ error cases (of which I’ve also written many) propagate ad nauseam from using APIs with un-typed errors specifically because you can’t know if you’ve handled all possible errors. This is precisely the situation I would like to avoid with typed throws.

2 Likes

This is such an important point; worth reiterating again and again.

Swift's wording makes this a bit weird to spell out since there's not a "Failure" thing in the language (fatal errors I guess), but generally this all loops back (to me at least) to the reactive manifesto we did in 2014: specifically to Failure (in contrast to Errors). You honestly would not want your system on every single possible call to expose super fine grained errors about "socket closed" "specific syscall failed" if the only thing you want to do is "my call went through or not"... you can try handling errors you know about, but unknown failures? Let it crash.

One can't really program against handling all possible "failures" (the network issues John mentions are a good example of "failure"); "errors" on the other hand like validation etc... Conflating the two kinds of faults, and making them typed makes the issue worse, not better; we lie to ourselfes we can handle all things, and we left no space for "panics" or "failures" if all we do is just typed or not typed (swift) errors. Proper "panics" that an actor could isolate would be a far more compelling story, completing the picture IMO.

Java with it's typed throws has numerous very annoying APIs which historically at some point were throwing "exception X" and today they don't, and never will; yet every single call site still has to assume it might throw it. Typed throws are a huge pain for evolution (in addition to that pretending we "know" all failure modes of a system -- we realistically don't...).

8 Likes

Typed throws (in my mind) has nothing to do with restructuring errors or enabling micro-optimizations.

To me, typed throws is all about guiding a client of the code down codepaths where they are not wondering "what do I do next?". Swift's strongly-typed language leads developers carefully down the path of showing them "here's what you need next ... now this ... now that..." because they must match the types. No matching types? No compiling code.

However, as soon as a developer interacts with an API that throws an error, Swift completely abandons them. Is this a CocoaError? Maybe a CKError? What about a URLError or an SKError? We give developers absolutely zero help with understanding what they're actually supposed to do with the error or even where to being looking for what might have gone wrong. We allow for the absurd situation right now where a networking library can throw any kind of error it wants, even if the error has absolutely nothing to do with networking. If I am implementing an authentication function, I could be anticipating that an error could be one of the two broad categories you define.

But what if it's not? As a developer, I would be given no indication that a "disk full" error is something I could reasonable expect as an error to be thrown. Maybe if I knew that I could quickly delete some cache files or something.

Typed throws is about more than just "micro optimizations" or whatever: it's about teaching users of the API what sort of things they can reasonably expect to go wrong, and therefore the things they can reasonably expect to handle.

Yes, this means that just about every Error-conforming enum/struct out there will have some sort of "unknown" case, but that is no different from the laissez-faire-you're-on-your-own approach we have to errors today. Since all do { } catch { } blocks must have a default catch statement, we have semantic equivalence.

With typed throws, we (as API authors) get a chance to explicitly describe what can go wrong and have the compiler enforce that error handling.

The current situation leaves API adopters out to dry with only the hope of header comments or documentation to tell them what to do.

We should be doing better than that.

28 Likes

What do your other catch clauses look like?

1 Like