Swift ErrorTypes – A Modest Proposal

After the WWDC announcement, when I got over my initial knee-jerk ‘Exceptions – ick!’ reaction, I came around to being a supporter of the Swift 2 error handling model. While it would be nice to be able to desugar the implicit result type, and there’s a glaringly obvious hole with async error handling, for general failable synchronous calls it works well. However, there’s one aspect that really bothers me. Consider the following code:

enum MathError: ErrorType {
    case DivideByZeroError
}

func divide(l: Int, _ r: Int) throws -> Int {
    guard r != 0 else { throw MathError.DivideByZeroError }
    return l / r
}

func calculate() {
    do {
        let result = try divide(10, 2)
        print("Result = \(result)")
    } catch MathError.DivideByZeroError {
        print("Can’t divide by zero!")
    } catch {
        // this should never happen
        break
    }
}

Errors thrown by a function are completely untyped, so Swift is unable to infer that the first catch clause is exhaustive – this means you ALWAYS have to include a catch-all at the end of your do block if you want to completely handle the error. This isn’t just a case of redundant code – if you add an error case later on, you’re going to suddenly (and silently) hit your ‘This should never happen’ handler, which is probably not what you’d expect (and seems out of step with the way switch statements work).

The other problem is that we have no idea from the function signature what errors we should be trying to catch. Apple introduced a Throws: keyword in the doc comments, but relying on the original developer to keep comments up to date can be… ineffective.

A number of people have proposed that throwing functions should be declared with a list of all the types they can throw, rather like Java’s checked exceptions. This will nicely solve our divide function example:

func divide(l: Int, _ r: Int) throws MathError -> Int {
    guard r != 0 else { throw MathError.DivideByZeroError }
    return l / r
}

func calculate() {
    do {
        let result = try divide(10, 2)
        print("Result = \(result)")
    } catch MathError.DivideByZeroError { // this catch is exhaustive
        print("Can’t divide by zero!")
    }
}

Which is great – we now have a sensible exhaustive catch statement, we’ll get helpful compile errors if we add a MathError case and forget to handle it, and the function definition is clearer and self-documenting. However, this is a fairly simple case, and the problems with checked exceptions were always much more evident with larger & more modular codebases – for instance, we’d start to see functions like:

func openDatabase(path: String) throws libuvError, URLFormatError, ZipArchiveError, SQLiteError -> Database {

Technically in this case we’d still be able to write an exhaustive catch without using a catch-all, but I can give a rock-solid guarantee that no-one ever will. A function signature like this leaks internal implementation details, and the exposed error types are very unlikely to be useful to API consumers.

Additionally, this can have an invasive impact on the rest of the code – consider the scenario where these error types are annotated on all functions that call openDatabase, then all the functions that call those functions, and so-on up the line until we have a monster catch (all) block. Now imagine we update our database code and add a new ErrorType, or change the zip library and that ErrorType is now different. We could end up having to touch a dozen files to cover a semantically unimportant change. It’s also worth noting that the Swift team have explicitly said they don’t want “pedantic lists of possible error types, like Java”.

Now this is a somewhat pathological case (although I have seen a lot of Java code written like this in the past!) and hopefully ‘good developers’ wouldn’t write code this way. The way this problem should be solved would be either:

  1. Erase the typed error information that’s not useful to consumers and just throw an untyped ErrorType – this brings us back full-circle – or
  2. Translate the internal errors into a meaningful new ErrorType, such as:
enum DatabaseError: ErrorType {
    case InvalidPath(message: String)
    case CorruptDatabase(message: String)
}

Ultimately, the more I’ve thought about this, the less useful a list of multiple ErrorTypes seems to be. However, I think there’s still value in being able to (optionally) annotate a single custom ErrorType, as in the case of the MathError or a consolidated DatabaseError from our openDatabase call. So my Modest Proposal is stated thus:

// allow us to do this:
func myFunction() throws -> Int

// or this:
func myFunction() throws CustomError -> Int

// but not this:
func myFunction() throws CustomErrorOne, CustomErrorTwo -> Int

A single custom ErrorType is much closer to the underlying language model, neatly complements the Swift 2.1 function covariance, delivers most of the benefits of typed errors, and limits the damage that slovenly error propagation can do to the codebase.

Advertisements

6 thoughts on “Swift ErrorTypes – A Modest Proposal

  1. Unless I’m missing something, this is good ol’school side-effecting exception throwing, right? I guess I’ll pass.

    A while ago I wrote a post about (somewhat) similar issues in error handling in scala and how union types would solve them: http://blog.pouria.me/2016/02/21/error-handling-union-types/

    And as a totally unrelated anti-swift rant, I think the following line sums up how stupid swift’s design is:

    `func divide(l: Int, _ r: Int) throws MathError -> Int {…}`

    It reads as if the function throws a function from MathError to Int. Surely, they could have come up with better syntax at least!

    1. Thanks for the comment Pouria!

      > “Unless I’m missing something, this is good ol’school side-effecting exception throwing, right?”

      No – ‘throwing’ a Swift error just returns it from the function. You can read:

      `func divide(l: Int, _ r: Int) throws -> Int`

      as syntactic sugar for something like:

      `func divide(l: Int, _ r: Int) -> Either<Int, ErrorType>`

      (with the notable caveat that you’re unable to de-sugar the return type).

      The restriction that I’m disappointed by is that I can’t annotate a function as throwing a more specific error – it’s always the top-level `ErrorType` protocol. The Swift team’s rationale for doing it this way is basically:

      1. Changing the error type will be a breaking change (the resilience model being a large focus for Swift 3)
      2. The majority of library authors will include a catch-all `UnspecifiedError` case to avoid breaking compatibility in the future
      3. Client code then ends up with something essentially identical to the catch all case anyway.

      I can appreciate the goal here – the resilience model is going to have significant benefits – but whenever I have to write a `catch { break }` to shut the compiler up it makes me sad.

      I liked your Ceylon union type example, but I’d be interested to know how it works out in practice. I’d considered whether something similar would work for Swift, but the boilerplate would be considerable (not as bad as Scala though), and as I outlined in my post, I’m unconvinced of the usefulness of propagating error types up the call stack.

      1. Just few quick points:

        1)

        I agree that having the signature of a function that throws errors explicitly indicate the fact via the `throws` keyword, combined with having to either handle or propagate the error at call site, mean that throwing functions in swift cannot really be considered to be side-effecting.

        On the other hand, as you mentioned, the imaginary `Either` result of a throwing function cannot be desugared, meaning for instance that I won’t be able to pass this `Either` as a parameter to a function. Therefore, a throwing function is (strictly speaking) less expressive than the version of the same function returning `Either`.

        2)

        My issue with your proposal (being able to specify only one error type, instead of multiple) is that it is unnecessarily restrictive and renders failable functions uncomposable: composing two functions with incompatible error types results in function that throws ErrorType. We’re back to square one.

        I say “unnecessarily” restrictive because the in a language/environment that allows multiple error types, the programmer always has the freedom to, should they choose to, specify a top-level ErrorType instead of a number of more specific error types.

        3)

        > “I’m unconvinced of the usefulness of propagating error types up the call stack.”

        I’ve wished enough times that I could take the guess-work out of knowing what error types may be returned by a function. Therefore, I’m in favor of propagating error types not only up the call stack, but also down it! And so far, union types seem to be the best way to do it.

  2. Hi Pouria, thanks for the response.

    > “2) one error type … renders failable functions uncomposable”

    Yes, I’d specifically like to avoid a model where the default behaviour from composing throwing functions results in unioned error types. A default of `ErrorType` with the freedom to create your own top level union type will result in the cleanest & most ‘honest’ code IMO.

    > “3) I’ve wished enough times that I could take the guess-work out of knowing what error types may be returned by a function.”

    I agree, it’s great to know this. The knowledge doesn’t give you much of a practical advantage though, if your top level function throws a union of twenty error types, each with at least half a dozen cases/codes. For typed errors to be useful, I think they need to be (manually) transformed into types relevant to client code, and automatically unioning types will discourage this.

    1. > “if your top level function throws a union of twenty error types”

      Chances are the top-level function is a leaky abstraction (and a code smell) if it may throw twenty different error types. Every layer of code should be engineered to simplify the world for the layer that sits on top of it.

      Simplifying the errors that may be thrown and making them relevant for the client of the layer is a similar engineering problem. It is exactly the same as making your normal (i.e. non-error) return types relevant to the level of abstraction of your client.

      In this way, I think the union types approach is really more ‘honest’, in the sense that if you can actually manage to get your top-level function to return a sensible number of errors, then there is really no way other (non-fatal) errors could be thrown.

      > “a union of twenty error types, each with at least half a dozen cases/codes”

      I wouldn’t use error codes in the union types based approach, as the whole point is to know at compile time (rather than runtime) what errors may be thrown.

      As for using error cases (by which I think you mean a small hierarchy of error types), I think they only simplify the union types based approach. For instance, instead of listing all the subtypes of a hierarchy you may simply use the super-type of that hierarchy. Similarly, instead of ‘catching’ or recovering from individual error cases, you may catch/recover from their super-type.

      > “For typed errors to be useful, I think they need to be (manually) transformed into types relevant to client code”

      That is a good practice, but in many cases not enough, specially when composing functions. For instance inside a function implementation I may need to use multiple other functions, each of which may return certain errors. Then I may want to compose their results together and send the result to a helper function that will recover from some of the errors. At this point only union types can tell me what errors are left uncaught.

      An example of this would be a web service controller: I’ve read the client’s input json, talked to an external web service and my own persistence layer. Now I need to know what errors are left uncaught. These uncaught errors will probably be translated to http status codes and sent to the client.

      1. Thanks Pouria,

        I agree with you on most points, it looks like the main difference is that you think all errors from composed functions should be unioned by default, and I think that all errors from composed functions should be untyped by default.

        In both cases a well defined, strongly typed error at the relevant layer of abstraction is possible. My view is that the former approach is likely to result in considerable over-specification of types in the vast majority of cases where these types aren’t used, and this will end up adding noise & interface fragility for very little benefit. I’m aware some people will prefer explicit typing everywhere (and this can still be done, albeit with a fair bit of boilerplate), but I don’t believe this will reflect the usage of the majority of Swift developers.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s