Precise error typing in Swift

I understand the framework argument. But this isn't code for a framework, it's already at the end of the chain: It's code inside an app project. I don't need to make my type frozen or add an unknown case, because when I change the enum (= add new cases), I can also change all callers of the enum (= the app code). And for this task, I'd actually be happy if the compiler failed to build the call site when I add a new case, because then I know exactly where I need to make changes.

4 Likes

Why is it public? That's what threw me. If you have no users other than yourself, then there are fewer problems. I think the argument for non-public typed throws is very strong, it's when you actually have users that things get complicated.

2 Likes

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.

22 Likes

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

5 Likes

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

11 Likes

+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.

15 Likes

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.

14 Likes

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

2 Likes

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.

10 Likes

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.

3 Likes

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?

5 Likes

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.

2 Likes

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?

18 Likes