Typed throw functions

Out of curiosity, may I ask what this addressing refers to?

I almost never talk about the design being a solution for problems with public API stability. It's true that the design does solve some of those problems, and it's true that you've always interpreted the design as focused on those problems, but it's never actually been the focus of how I think or talk about it.

The way people write error handling — almost necessarily, regardless of whether it's an API boundary or not — is to (maybe) handle a few special cases and then treat the rest uniformly. There are very few systems in the world which actually have a tiny number of different failure conditions, and even with those systems, it is almost always still correct to write error-handling code that handles a few special cases and then treats the rest uniformly. In a world where that's how you write error-handling code, typed errors are ~90% documentation. And it's a special kind of documentation that induces otherwise-reasonable programmers to spend large amounts of time and mental energy solving non-problems.

6 Likes

I think your misunderstood the Network layer error system. We receive errors from nsurlsession (not throwing) in a function that transforms (not wraps the original error) into a list of more generic, less broad, and (I wish) typed error. Why? Because the consumer of that function, which is a layer above, needs to know which Error was thrown and I hate to have the generic catch empty, just in case, after catching my error (casting it from the generic error). I know it won't of through that generic catch because I know the type of the error (I've done the API or I have to check into the docs) but what if someone by mistake throws the NSURLSessionWhicheverError? Then the issues start, we have to debug all the stack to find who were the responsible of not following the error API we provided, and he could do it because we cannot restrict the type in any way, so that error goes to the generic catch that usually is empty or prints the error in this case instead of formally handling it properly.

I expect to have explained myself correctly.

1 Like

By "other catch clauses" you mean ones where I'm catching an error that didn't need a catch-all clause?

Ok, an example from my SNMP MIB compiler:

do {
            try parser.parse(module: text)
        } catch let error as CompilationError /* this is the only type of error thrown */ {
            let token = ErrorToken(error, module: String(parser.name))
            self.errors.append(token) // for presentation to the user
            return
        } catch {
            // If we had typed throws I wouldn't need this
            fatalError()
}

And for the other case, an example from some file handling code where I had to create an unknownError case, showing how this unfortunate reality propagates from untyped errors (and poor documentation).

do {
    size = try (FileManager.default.attributesOfItem(atPath: location.standardizedFileURL.path)[.size] as! NSNumber).int64Value
} catch let error {
    // no idea what can be thrown here, so I can't do any meaningful handling
    // I have to just abort the entire data storage operation
    close(fileHandle)
    throw SNMPSensorError.unknownError
}
5 Likes

Ok, I guess I just disagree with you or see the world differently there. Particularly in systems code there are many cases where you'd dealing with a closed world, and it is the norm that you can enumerate all of the conditions, and if you want resilience and extensibility, you manually type erase. I am not the only one that sees it this way either, I'll give you two examples:

First, look to the design of the Rust language and its error handling: that community is strongly focused on this sort of use case, and independently came to the same conclusion. Other than this difference, their error handling approach has converged to something very similar to Swift's.

Second, you can see this in the comments above - people in this space are just using Result manually instead of error handling. I seems that your preference would be for people to use throws and deal with type erasure, but we already have signal that people have considered and rejected this. The impact of this is that these people have to use manual error propagation, something that leads to bugs and is not ergonomic.

To be clear, I'm not arguing that we change the default here! Only that there are really important segments of the world that Swift is not covering very well. The solution here is technically straightforward to solve, and it doesn't make Swift any worse for existing users. I don't understand the opposition here other than "someone might design an API wrong" -- but we provide plenty of rope for people to design bad APIs already.

-Chris

52 Likes

I can only share my opinion as a user of programming languages and as an application developer.

There are times when you need explicit errors and there are times when it's feels like overkill. On complex projects I tend to have explicit errors especially when errors are thrown through multiple layers. You will loose track of the possible error scenarios on upper layers quickly and documentation will get out of sync quickly (after all we have a compiler to make us aware of these errors). In my current project we switched from loosely typed errors on functions to explicit errors because we did not understand what was going on anymore.

We really need to differentiate between Result and throws. throws is syntactic sugar for the except monad (Haskell as academic reference: Control.Monad.Except). Semantically you can write it with Result, but most of the time the code will consist of a lot of chained operators. Chained operators everywhere is maybe not what Swift was meant to be (e.g. we can't pattern match in expression style but only with statements).

So with all this type safety in Swift I would argue for a more powerful throws that is comparable to Result and Combine-Types.

But speaking about explicit errors is only half of the problem, because we need to talk about representing sets of errors and having type operators on these sets as I posted here already: Question/Idea: Improving explicit error handling in Swift (with enum operations).

3 Likes

My guess on summarizing the issue: both typed and untyped throws have their uses. The problem with picking a philosophy is that if your use case in the wild ends up making the chosen philosophy the wrong bet, the consequences are devastating. (Huge type lists and/or catch-all error values if typed throws was wrong; exhaustive testing to counter type erasure if untyped throws was wrong.)

Is there any way to moderate the consequences of making the wrong decision?

Is it that that common that your systems code has the same enumeration of errors from method to method? In POSIX for example, there are a fixed set of errors which:

  1. Most functions only return subsets of error values
  2. May require additional calls to determine next steps due to coarseness of errors
  3. Interpretation of the errors varies function to function
  4. Wind up being extended with new values for different operating system needs based on say the underlying architecture or functionality

I would argue that fixed errors which may be returned by a function typically only have meaning in the context of that function. To have business logic in the calling function try to return a typed, fixed set of error values based on the errors you receive from the calls it makes usually requires both a translation to an appropriate error from your function, plus the context of both the original error and state which led to that error.

So IMHO the effective use of typed throws would be limited to you expect the calling business logic to try and insulate its callers from details of the error. For low level logic which is not integrating a lot of systems, this could be a significant body of code. For an app developer, trying to do this is a big waste of effort better spent elsewhere.

To that end, typed exceptions (and Result with a subtype of Error != Never) make sense to me within a module’s bounds.

In the end you have the exact same issue with Result, if you end up not typing errors, maybe you aren't able to handle them at all in your whole application. If you type them, maybe you end up with a bad API design. There's no correct answer to your question, because both paths can end in the same end. This is not something (IMO) should be addressed at language level, as it wasn't when Result was introduced. This is something developers may address in their jobs/hobbies/programs/whichever, and try to make the right call from the beginning.

One thing to say about typed throws is that you can erase them at any point of the program or even drop throws because you don't need it anymore. It is more flexible in that regard. When you commit to Result it is hard to drop because it sudden appearance or absence on a API can create a lot of confusion.

The closed world is the easiest case. If you don't care about breakage, then everything is possible. It is my understanding that Swift considers such closed environments to be the exception - that is, as I mentioned before, that the defaults should be as flexible as is reasonably possible, and developers can trade away that flexibility via the use of attributes like @inlinable, @frozen, etc.

The biggest danger, to be frank, is that users seem to be drawn to adding types to things almost like an ideology. It does allow some boilerplate code to be eliminated, which is nice, but it isn't true that more specific types are always better.

Errors are just fundamentally different to a function's input parameters or its results; they encapsulate information about how the result is computed, which specific sub-step failed. They pierce a hole in the abstraction provided by the function's interface, so the cost/benefit consideration about locking down a function's error type is very different to its parameter/result types.

8 Likes

I really think that this assumption makes the Swift developer a machine, which isn't. When async gets finally implemented would every programmer make all his asynchronous functions async? probably not. Probably someone prefers RxSwift, or Combine or any other RFP. Did Result kill throws when it got released and no one never used throws again? Or when guard was implemented?
I think you got my point, it is developer's responsibility either to maintain or not a ABI stability of their libraries as they should be free to reduce the world of possible Errors in his domain (if we enter into algebra) to whichever they think it's more convenient for their APIs or libraries or applications, etc.

Add possibilities it is not a risk, it is an investment.

1 Like

Adding possibilities is risk and investment. Swift developers are not mindless machines, but we're not hyper-rational specie either. Many won't contemplate whether to use throws or Result and just grab whatever feels lighter. Some will grab a boilerplate they learnt from some random website many years ago and stick with it to death, etc. That's why it's a vital philosophy to salt bad code.

Now, that's just a general rule for any addition, not that it applies heavily to typed error since it still fills some critical gap, like how enum fills the gap for class/struct. That's why I think it's essential to have some kind of guideline if we add typed error. In fact, we already have one before it: to use Optional when there's only one case to fail use throws when there are multiple ways to fail. Though it is indeed easy to get defensive and use typed error far too extensively.

4 Likes

That's a good observation. I share this one.

But what about code that's under your control and where you/your team want to safeguard yourself from overlooking unhandled error cases? Then it's less about a general "what the Swift community wants from an API" discussion and more a discussion about giving developers choices.

3 Likes

For me it's clear: fills the gap between no Error and a ConcreteError (throws with and without explicit type)

And now that everyone of us put all this effort into this thread, is there a next action item or do we put it aside for the next 3 years? Do we need other feedback or evaluation before writing a proposal? Just curious :thinking:

1 Like

The next step would generally be for some motivated individual (or individuals!) to write up a draft proposal/pitch and post it in Pitches so that the feature can be discussed in the context of a concrete design.

2 Likes

I'm interested in this topic for so many years that I feel finally motivated enough. As part of a team or if necessary on my own. Someone wants to join?

2 Likes

As I said, as original post, I'm eager to write down a pitch, but I was looking for some help to do it.

If someone is interested, can be published here or sending me a private message.

Your question illustrates exactly the problem that I'm trying to highlight. There is no one class of API in the world. Different APIs have different constraints, Swift shouldn't try to force square pegs into round holes.

To reunderscore the point, low level libraries end up being very different than application level frameworks: and in many cases it isn't /useful/ to allow evolution of failure models in this code. Take a look at one familiar body of code: the Swift standard library. It currently only throw two different concrete types of errors: DecodingError from codable stuff, and UTF8ValidationError from string stuff (note that the codable API is an example of where untyped throws is really important, because it calls into client code that can throw domain specific errors).

My point is that the Swift standard library is a large body of code that mostly doesn't need this, because Swift already has a way of typing the failure models: it already supports returning a value as an optional instead of throwing an error. The many APIs that use this are already admitting that they are closed to error handling evolution.

There is a wholly separate discussion about "how to be design a specific API". I agree that we don't want to see adoption of typed errors as the default for large scale APIs, but remember that we already have this problem: APIs can be built to return Result today.

-Chris

16 Likes

Typed errors might not be the best answer to the problem "I don't know what errors this API throws", but relying on documentation is far worse. The compiler knows what errors are thrown and that is the only source of truth. Documentation quite often gets stale and does not reflect what the code is really doing. Reporting this somehow should be the compiler's responsibility. I haven't thought it through enough to propose something better than typed errors to solve this problem, but I think it's still better than what we currently have.

3 Likes