Precise error typing in Swift

As long as the language offers an easy, automatic way to erase the error, I really don't see the problem here. As a framework author I'd rather offer precise errors the user can erase with a standard mechanism than erased errors the user has to recover by knowing about my error types.

IMO, it's simple enough to have throws and throws ErrorType, where any throw of a typed error from within a throws function will automatically be erased to Error.


It's public because I'm modularizing my app. See here: Episode #171: Modularization: Part 1


This is the main reason I want precise errors: they make reasoning about specialized micropackages far easier.


+1000 on this - I work heavily on both local and shared Swift packages. I'd love to be able to provide async APIs with strong error type guarantees - currently I have to use Combine or Result types for this.

Will the consuming clients erase the errors downstream in their error handling pipelines? Maybe. But at least I can give them an API error contract that's enforced by the compiler.


Even if the throwing function is in a public library, and even if the typed error enum isn't @frozen, it's still adds value for framework users, non? It gives me a great starting point to discover common, high-level errors that may require special handling or user interactions.

I'd much rather have the compiler tell me that the common errors are file-not-exists, file-is-dir and that I also have to deal with @unknown errors, rather than having no help at all.


I just wrote a pitch kinda related to this thread (it's about Result, actually), see here:


Are there still plans or work in progress for this? This, the @rethrows attribute, and the lack of a primary associated type for AsyncSequence means that certain APIs are currently impossible or unwieldy. Take for example a redux-like system with the following protocol.

protocol Reducer<State, Action> {
  associatedtype State 
  associatedtype Action

  func reduce(state: inout State, action: Action) -> any AsyncSequence<Action, Never>
1 Like

There hasn't been any concrete action here, no; the team has been wrapped up in other work.

One thing that caught my attention recently was the distributed actor implication for error handling.

A non throwing distributed func foo() on a distributed actor becomes throwing. Does it mean that a distributed operation cannot ever have a concrete error type and must erase any error (if any) to Error?

Follow up questions. Now that we have any and some:

  • do we now throw any Error?
  • could we have opaque errors and throw some Error with typed error feature?
1 Like

In principle, I think the ad hoc checking for distributed code could merge the error types from the actor method and the actor's chosen transport protocol, so if they agreed on a non-Error error type, you wouldn't have to erase all the way to Error.

That said, a network service seems like exactly the sort of situation for which I have argued before that trying to lock down the error type is usually counter-productive, and you are better off just using Error.


Locking down error types across network boundaries is a mess imho and counter productive. but if we had to be more precise that’s why the distributed module includes a “DistributedActorSystemError” protocol to which errors thrown by the actor system (transport) should conform… in theory then actual errors then a would be “SomeErrorTheFunctionThrows | DistributedActorSystemError” but I know we’re missing union types and there did not seem to be much love for them in the team, at least last time I mentioned them.

In concrete runtimes (the cluster) we even require allow-listing error types before we try to encode them, as to not accidentally leak information to remote callers.

Having that said, the less tied up remote peers are in exact impl details of another the better; and especially errors are not that helpful imho.

1 Like

This recalls the discussion and proposal made here time ago. With the introduction of Concurrency, consumers keep losing information about error handling and it's inevitable in a way or another for Swift to provide a visible error handling interface as it does with Result and Combine.


It’s not clear why you think it’s “inevitable”?

It's my own opinion but the current throw system is totally erasing sufficiently error information wether his rivals are specifically typing it. There are technical reasons to go for Concurrency, but error management is not over of them vs its competitors that, surprisingly, were added when the language got certain maturity. Why should I have to choose between typed or not typed error handling depending on the framework or API that I'm consuming? There are many explanations of this in the linked pitch so I think I shouldn't extend much further about my opinion.

There’s a difference between losing error information and not exposing it in the type system. Swift wants you to handle things like enum cases exhaustively. It would be a serious ABI stability challenge if APIs were locked to a fixed set of error types forever.

When you control both sides of the ABI (like the interface between your model and view code), it’s not as big of a deal.

How would it be any different from locking a return value to a fixed type?

func getFoo() throws -> Foo

is homomorphic to

func getFoo() -> Result<Foo, Error>

Specifying a precise error type as the Result parameter would present the same sort of ABI stability challenges as the Success type. Am I missing something?


It’s not. Return types are part of the ABI. Opaque return types give implementations the ability to change their concrete return type as they evolve. Encouraging people to use more specific error types is a motion in the opposite direction. It increases fragility, which is tolerable only when you tightly control both sides of the interface.


I don't see it as moving in the opposite direction, so much as filling out the type-safety story of Swift. It has always struck me as odd that one of the main motivations for the creation of Swift was Objective-C's lack of type safety (id has entered the chat). Yet in Swift, we've somehow ended up in the place where we want descriptive types everywhere ... except with our errors?

Adding precise error typing would not stop anyone doing func getFoo() throws; we would still deal with any Error values just as we currently do. But for the API authors who can make guarantees about the kinds of errors we produce... why should we preclude that?


I’m not saying we should! I’m saying that even if the language gets the ability to specify tight return types, it’s actually not a good idea to use them in many real-world cases.

To give a concrete example: for years the only storage on an iPhone was local NAND. If Foundation were being developed concurrently in Swift, one might advocate for giving NSData’s file reading errors a more specific type, such as FileSytemError, since that describes the sole category of errors that could occur on an early iPhone.

Then the iPad came along, along with the Camera Connection Kit. Suddenly iOS devices could have removable storage. You could try to retrofit this concept into FileSystemError by extending it to cover the newly-possible case of a filesystem completely disappearing in the middle of access.

And then iCloud file providers appear. Suddenly your filesystem access can fail for network-related reasons. Do we extend FileSystemError to now cover all possible underlying network-related errors, and if so, do we duplicate NetworkError’s API into FileSystemError, or do we give FileSystemError the ability to “wrap” a NetworkError? Are any of these changes even possible to make in an ABI-compatible way?

In complex systems, API surface methods have much more control over the values they return on success than they do over the reasons they can fail. Swift’s current design, privileging concrete return types and abstract error types, reflects this.


Erasing to Error doesn't provide the stability you seem to think it does. Unless the errors are themselves entirely opaque as well you're going to run into the same ABI and source stability issues that you would if you returned an error type directly. Otherwise how do you expect people to change behavior depending on the specifics of the error? Unless all you care about is the fact that an error occurred, which try? already handles, you're going to cast to specific error types and look at the specific case information.

In any case, Swift already handles the example you give by supporting frozen enums. Evolving the underlying error in that case is as simple as adding cases to the enum, as the language has already required the use @unknown default by the user. Of course it would be nice if this feature could be adopted by non stable libraries for things like errors, but the biggest issue, ABI stability, is already solved.

In short, this benefit you claim for erased errors doesn't actually exist.