Typed throw functions

Hi Swift Forums!

Nowadays, a throwing function is mean to throw anything that conforms to Error, which can make catch clauses unnecessarily complex.
This is a proposal for let functions marked as throws to specify which Error will be throwing, and this will be compiler type-safety backed up such:

enum AuthenticationError: Error {
    case invalidCredentials
    case unknownReason
}

func authenticate(with username: String, and password: String) throws AuthenticationError {
    [...]
    throw .invalidCredentials // can be inferred by the compiler due to the function signature.
    throw NSError(domain: "my domain", code: -1, userInfo: nil) // error: NSError cannot be casted as an AuthenticationError
}

do {
    try authenticate(with: "foo", and: "bar")
} catch {
     print(error.localizedDescription) // error is a AuthenticationError
}

do {
    try authenticate(with: "foo", and: "bar")
} catch let error as NSError { // error: AuthenticationError cannot be casted to NSError
     // N/A
}

Maybe this is an old topic but seems interesting to revisit it and see how the type inference system is much stronger now and that error handling is somehow incomplete; in a language where the strong type system is one of its clutch, this topic seems very poorly treated and hard to follow when there's a heavy throwing reliant topic.

Much appreciated,

Jorge

Pitch draft: swift-typed-throws/SE-XXX Typed throws.md at master ¡ minuscorp/swift-typed-throws ¡ GitHub

23 Likes

IIRC, it started as "there's no reason for typed errors", not a problem with type checker, and see if users need more (which is precisely this thread).


Personally, I still feel that it's a burden to annotate error types Java-style.

4 Likes

I have read the rationale, doesn't seem to cover this kind of cases. Also, I'd like to add that this is all backwards compatible so you, for example that you don't like this type of annotations can just use the current semantics without issues.

8 Likes

There’s been quite a lot of discussion on this topic already:

It might be worth going over that before restarting the conversation.

6 Likes

I've read all threads, from 2015 up to 2017. Swift has changed a lot in three years and I think that with Swift 3 on the future road, this could be a good moment too have a specific discussion on how would this be implemented, limits (does generics makes sense?), and congruent with the actual semantics.

It seems fair to raise this topic again in 2020 with a totally different Swift and a stronger community.

Also thank you for the recap of all of those threads, some of them inspired me to open this and go forward.

5 Likes

Thanks for the effort! I think it'd be helpful/informative if you summarise the threads, and note how things have changed. The latter of which is quite important.

4 Likes

A strong argument for introducing typed throws now is because Combine uses typed errors, and working with combine and non-typed throws is awkward and leads to annoying boilerplate. On my phone now, but can write som example code tomorrow.

14 Likes

For what it’s worth, I mostly stopped using throws when Result was added to the standard library, precisely because it self‐documents what can go wrong, and can be easily switched over to get the master list if the error is an enumeration.

The only time I still use throws is when I don’t even have information about the possible error types myself, because the method only passes on failures from Foundation or some other library with untyped errors.

12 Likes

I agree that Result in several cases is better in today's world - but I don't see it playing well with the language at a native level once we finally get async/await by Swift 6.

It'd be great if the type system itself absolves the need for an actual type, for both the asynchronous and synchronous worlds.

2 Likes

Whenever I see things like this, showing the versioning troubles people are already having with errors, I’m glad we don’t have typed throws.

Specifically, things like this:

  • Using enumerations to represent Error s is inadvisable, as if new errors need to be introduced they cannot be added to existing enumerations. This leads to a proliferation of Error enumerations. "Fake" enumerations can be made using struct s and static let s, but these do not work with the nice Error pattern-match logic in catch blocks, requiring type casts.

At least they have the escape hatch of multiple enums! It’s not pretty, and the better approach is to use a struct, but people use enums anyway :man_shrugging:

Could you imagine if we had this, and every library had been striving to include the error types of throwing functions as part of their signature? The ecosystem would be much more fragile.

I would consider the pitch I just linked a prerequisite for even considering typed throws. Otherwise it’s too dangerous to be made so easy. In the mean time, those who really want it have Result.

Additionally, as part of typed throws, I would consider making any enum which conforms to Error resilient by default (with an ability to opt-out by marking it @frozen). Thrown errors will be gaining ABI significance, so it’s important developers think about this.

1 Like

I don't see any issue with what your statement tells. Is therre any issue with making a switch? Even further, that issue, if it is, it can occur now with the standard error catching but multiplied to infinity enums that conform to Error, so I rather stick with just one.

1 Like

Can you describe how do you use Result when you have several result statements chained in a function? Do you unwrap all .value? what happens with the Errors? Are ignored? Doesn't feel like a safe approach but I'm open to have an example.

Just to recap what some people with strong knowledge of the language thinks about this:

There're strong opinions on the fact that typed throws are a thing to include into the language and that even it could be ABI backwards compatible using the introduced semantics. For record that someone has it here to read and have more context about this.
Also, I'd like to see more dos and don'ts with the proposal in code in order to see what issues the semantics would have so we can analyze edge cases and other maybe related topics.

All contributions and opinions are more than welcome!

Thank you.

Some topics to cover:

  1. Generic throwing allowed
  2. Protocol establishing Error type through associatedtype.
  3. Extensibility of errors.
  4. ABI compatibility
  5. Semantics
  6. Different Error do statement.
6 Likes

Would it make sense to introduce an ‘@unknown default:’ counterpart to catch blocks. That is, if a library offers an Error enumeration that is not marked ‘@frozen’ the compiler will require a typed throw to add an ‘@unknown catch’ clause:

func throwingFoo() throws Bar { ... }

do {
    try throwingFoo() 
} catch Bar.baz { 
    // Handle the baz error 
}

❌ Not all cases of ‘Bar’ are handled
      Add ‘@unknown catch’ clause

So to correct it we’d write:

do { 
    try throwingFoo()
} catch Bar.baz { 
    // Handle the baz error 
} @unknown catch { 
    // Handle unknown cases
}

✅ Compiles

Filip

3 Likes

You can always fallback to default which is

do {
    try throwingFoo()
} catch {
   // You can switch on error: Bar
}

or

do {
    try throwingFoo()
} catch Bar.baz {
   // Handle Bar.baz
} catch {
   // You can switch on error, which has every case, even @unknown, except from Bar.baz because
  // it was previously catch'ed.
}

The main good thing here is that just an empty catch will gather all errors regarding Bar's enum, and use a normal switch statement.

6 Likes

Note that this equivalence between rethrows and generic thrown types is invalid, due to eg. this function, which is valid today:

struct Err: Error {}
func myMap<T, U> (x: T, f: (T) throws -> U) rethrows -> U {
  do {
    return try f(x)
  } catch {
    throw Err()
  }
}

I don't see your point:

Your snippet does not include any type of typed throw clause, so your code will be able to throw and rethrow anything.

@Joe_Groff I think meant to say that error type indicated in the generic function signature as:

func foo<T>(_ block: () throws T -> Void) rethrows T

I've updated the code to reflect the current Swift semantics.

The issue is that the catch block throws an error with a potentially different dynamic type from the one thrown by f. It used to be that rethrows functions could not throw their own error at all, but it looks like that has since been fixed.

2 Likes

So the signature in

Could be potentially valid in the new semantics.

I've deleted that T: Error because T needs to be an Error by compile-check because is after a throws statement, same for rethrows

The function ends up as a chain of map(_:) calls.

I would say the majority of top‐level calling code that won’t pass on the error just hangs .get() onto the end of the operation to turn the whole thing back into a throwing expression. However, a minority of call sites do want to handle particular errors, and for those I’ve found the Result style to be valuable for several reasons:

  • As the library author, you don’t have to separately document what can go wrong (or worry about forgetting to).
  • As a client developer, you don’t have to search for documentation to find out how to spell what it is you are trying to catch; code completion and exhaustiveness fix‐its will just tell you.
  • As the library author, you cannot accidentally forget to make the error type public (whereas throws will let you throw an internal type, which no one can catch).
  • Adding, changing or removing an error identifier is still a source‐breaking change of semantics, even if it is just string identifier or an NSError’s, error code that changed. In my opinion, you may as well let the API reflect it.

Admittedly, I would probably see the whole thing differently if you couldn’t go back and forth between the two styles so easily. The only annoying thing I encounter sometimes is that .get() doesn’t inherit @discardableResult, forcing you to use _ = more than I would like.

2 Likes