Typed throw functions

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

To me, this suggests that that typed throws would work better in a language with union types/anonymous sum types:

func throwingWithCallback<E>(callback: () throws E -> Void) throws (E | SomeSpecificError)

But anyway. Like all positions on typed errors, this applies equally to Result types. Regardless of whether typed errors are good or bad, having different choices for throws and Result is an unpleasant wart, and we have examples in this thread of people choosing Result when throws would be more ergonomic purely in order to use typed errors. If the goal of not having typed throws is to prevent people from creating bad APIs, it has clearly failed.

7 Likes

Just want to announce that @torstenlehmann and I have been debating into creating a pitch and here you have the first draft. I'd gladly expect feedback and feel free to make pull requests into it. When we feel it is finished we can go through the pitch forum!

2 Likes

It does. It's quite okay in TypeScript so far as shown here: Question/Idea: Improving explicit error handling in Swift (with enum operations)

In this post I try to evaluate how that could look for Swift with enums.

In Objective-C, all current throwing functions had an implicit type in the function signature: NSError , and, as has been pointed out, this does not provide more information that the current generic Error . But developers were free to subclass NSError and add it to its methods knowing that, the client would know at call time which kind of error would be raised if something went wrong.

How so? As far as I know there is no way to mark that obj-c method throws at all, let alone what specific type it throws

#import "MyClass.h"
#import "MyError.h"
@implementation MyClass
-(void)testMethod {
    // there are no indications at call site
    [self throwingMethod];
}
// there are no indication of throwing at the method definition
-(void)throwingMethod {
    @throw [[MyError alloc] init];
}
@end

The point was the lost of the type of the error, I know that there wasn't any way to throw of course! :smile:

Ah, but there is a way to throw in obj-c! It's even less typed than the one in swift. Try adding the code snippet above, and these supplementary files, and call testMethod from somewhere :)

MyClass.h

#import <Foundation/Foundation.h>
@interface MyClass : NSObject
-(void)testMethod;
@end

MyError.h

#import <Foundation/Foundation.h>
@interface MyError : NSError
@end

MyError.m

#import "MyError.h"
@implementation MyError
@end

Ah! I think by "current throwing" you meant "throwing in swift" and "ones that take a double-pointer to NSError"

Could you rephrase that? Obj-c functions are as current as the swift functions :)

1 Like

It was not a common pattern in Foundation AFAIK, but indeed it works. So let's step up on what ObjC can do and add a type to the throwing pattern.

I would like to have seen some type inference in the proposal as it would have been a pretty strong motivator:

enum Foo { case bar, baz }

func fooThrower() throws Foo {
    guard someCondition else {
        throw .bar
    }

    guard someOtherCondition else {
        throw .baz
    }

    ...
}

Also:

do { try fooThrower() }
catch .bar { ... }
catch .baz { ... }

Furthermore, since Result already exists and is a perfectly valid type, I think integration with the new Typed Throws feature is a given. Namely, the ‘get’ method should be typed:

extension Result {
    func get() throws Failure -> Success 
}

Lastly, I think some more information on the source comparability compatibility effects of ‘Typing’ and ‘Untyping’ ‘throws’ would be much appreciated by the community. This, of course, isn’t a very complex behavior, however failing to mention it seems irresponsible. Moreover, Library Evolution didn’t seem to be given much thought with no mention of how non ‘@frozen’ enum error types would be treated. Discussing this, though, is integral to its adoption by libraries. There is an existing behavior in place that forces non ‘frozen’ enums to be handled appropriately in ‘switch’ statements (the compiler forces such switch statements to add an ‘@unknown default’ case). I think discussing this point in the proposal is crucial.

Other than that the proposal looks quite good at first glance.

(There’s also a typo, you wrote: desbalanced)

5 Likes

You made very good points that indeed will be covered in the pitch. Thank you!

Could you extend a bit more what do you try to explain with the comparability?

I have a couple other questions I'd like to see addressed in the proposal:

  1. What are the subtyping rules for typed-throws function types? E.g.,
class BaseError: Error {}
class SubError: BaseError, Error {}
let f1: () -> Void = { ... }
let f2: () throws SubError -> Void = f1 // OK?
let f3: () throws BaseError -> Void = f2 // OK?
let f4: () throws -> Void = f3 // OK?
  1. Do we ever infer the throws type?
enum E: Error {
  case failure
}
func f<T>(_: () throws T -> Void) { print(T.self) }

f({ throw E.failure }) // Does this compile? What does it print, if so?
5 Likes

I am strongly in favor of supporting typed errors, but I am strongly opposed to supporting any attempt to support throwing more than one error. Such a thing breaks consistency with Result, and adds significant complexity to the language.

You can argue that such an extension improves the expressivity for certain cases (e.g. the one above) but I think such an extension would be a bridge too far.

-Chris

13 Likes

Yes we thought about that but consistency with Result takes priority because that will lead to the possibility (at least at some point) to use throws and Result interchangeably. We are already working on a section in the draft mentioning this.

(Meant to write compatibility there; sorry for any confusion.)

To elaborate on my point I think the proposal should explicitly mention how changing a ‘throws’ from a typed to an untyped form and vice versa will affect a library’s source comparability.

For instance, consider this function:

func fooThrower() throws 

Are we allowed to change it to the following - while remaining source compatible?

func fooThrower() throws Foo

My intuition tells me “Yes!” - since Foo is an ‘Error’ after all. So let’s see, the code for handling the former function would probably look like this:

do { try fooThrower() }
catch let error as? Foo { ... }

Which means that by changing to the latter function clients rebuilding will get a warning (saying that error is Foo). That, however, doesn’t really sound like source compatibility. Hence, I think that explicitly stating that to API designers is important.

Furthermore, while on the note of exploring source compatibility effects, a brief mention to the source breaking effects of typing a throw would fit in well, in my opinion.

The other thing I was concerned about in regards to source compatibility was the handling of non ‘frozen’ enums in library evolution mode. I’m glad to see, though, that this issue has been addressed in the proposal.

3 Likes

Even if the type would be a variadic generic enum with index enumerated cases provided by the stdlib that itself conditionally conforms to Error when all type parameters conform to Error?

3 Likes

There are some cases where this might not work out so nicely. Consider:

func fooThrower() throws {}
func generalThrower() throws {}
var throwers = [fooThrower]
throwers.append(generalThrower)

Today, the type of throwers is inferred as Array<() throws -> ()>, but with typed throws, if we changed the definitions to:

func fooThrower() throws Foo {}
func generalThrower() throws {}

then throwers would presumably be inferred as Array<() throws Foo -> ()>. Then, the line throwers.append(generalThrower) would become an error, since generalThrower may throw non-Foos!

3 Likes

As far as I understood @filip-sakel we should collect these issues and just formulate a guideline for API implementers. But yes updating a function that throws a Swift.Error to a more specific error needs to assumed as a breaking change.

4 Likes

How to crush my mind in one sentence. Can you give an example? :smiley: