Skip to content

Instantly share code, notes, and snippets.

@nicklockwood
Last active August 13, 2020 13:57
Show Gist options
  • Save nicklockwood/21495c2015fd2dda56cf to your computer and use it in GitHub Desktop.
Save nicklockwood/21495c2015fd2dda56cf to your computer and use it in GitHub Desktop.
Thoughts on Swift 2 Errors

Thoughts on Swift 2 Errors

When Swift was first announced, I was gratified to see that one of the (few) philosophies that it shared with Objective-C was that exceptions should not be used for control flow, only for highlighting fatal programming errors at development time.

So it came as a surprise to me when Swift 2 brought (What appeared to be) traditional exception handling to the language.

Similarly surprised were the functional Swift programmers, who had put their faith in the Haskell-style approach to error handling, where every function returns an enum (or monad, if you like) containing either a valid result or an error. This seemed like a natural fit for Swift, so why did Apple instead opt for a solution originally designed for clumsy imperative languages?

I'm going to cover three things in this post:

  1. Why the Swift designers are saying that Swift's new errors are not really exceptions, and why that is both true and false.

  2. Why the result enum approach to error handling is not a universal solution for error handling in Swift.

  3. What I would have done instead.


  1. The Exception That Proves the Rule

Why would Apple tell us that exceptions are a bad way to handle errors, and then introduce exceptions as the way to handle errors in Swift? Is this just another example of the reality distortion field that brought us "7-inch tablets are too small", "5-inch phones are too big", "Dynamic dispatch rules, vtables drool", etc?

Yes and no. Exceptions are/were deemed as bad for two reasons, one technical and one usability-related:

The technical reason is that the stack unwinding triggered by an exception either causes memory leaks due to skipped release statements, or imposes runtime performance costs and binary bloat if the necessary code to prevent these leaks is generated by the compiler for every method.

The usability reason is that it is not apparent when calling a function whether it (or or one of the functions it calls internally) may throw an exception, and so it is hard to reason about whether you've handled all possible error scenarios, short of wrapping every call to third party code in a try/catch (this also ties into the performance problem - if we knew which methods could and could not potentially throw exceptions, the code could be optimized).

But the latter of these two problems was actually solved long ago in Java by the use of checked exceptions, which require that every method that can throw an exception declares this fact in its interface, as a sort of alternative return value.

And Swift's exceptions are exactly that: they are just syntax sugar around a new kind of return statement. There's no expensive and complicated stack unwinding as the program jumps out of its normal flow, skipping stack frames until it reaches the nearest catch statement; Swift's solution adds no more overhead than the old NSError pointer arguments in Objective-C, and in fact seems to piggy back on the same mechanism (or is at least functionally compatible).

But, implementation aside, Swift's errors have almost the exact same syntax and semantics as Java's checked exceptions (bar some tinkering with the way try is used) and are exactly as cumbersome to use, but without the performance costs that Java inherited from its earlier, unchecked behavior.

So "don't use exceptions for flow control" was not some kind of general statement of good programming practice - it was purely a matter of technical convenience. They're telling us that Objective-C's exceptions are inefficient, and you shouldn't use them, but Swift's are OK.

And are they OK? Well, we'll get to that.

  1. The Functional Imperative

Repeat after me: "Swift is not Haskell!"

Swift's methods have side effects. Swift's objects have mutable state. Singletons are alive and well in Swift. Swift doesn't come with lazy-by-default evaluation, or monads, or signals, or promises, or the "await" keyword, or Haskell-esque "do" statements. Writing pure functional code in Swift is awkward and impractical because none of the system APIs you need to use to actually get shit done have any interest in your functional aspirations.

As a community, we embraced result enums in Swift because they're a lot nicer to use than the old Objective-C approach of passing a pointer to an error and checking for nil or false results. But let's be honest - a good part of the reason for that is that Swift makes the old pattern considerably more awkward to use than it was in Objective-C, thanks to Optional unwrapping, stricter type checking, and obfuscation of pointers. Passing a null pointer into a function and checking if it's still null when it's finished is just not the Swift way to do things.

I'm not saying that result enums are not objectively better than error params. Just that the reason people like them in Swift has as much to do with convenience as it does functional purity or ensuring code correctness.

Brad Larson wrote a good article about why he liked result enums, and why he's switching to the new Swift 2 error system anyway, and the answer is basically because it lets him do the same thing with less boilerplate.

When push comes to shove, convenience trumps functional purity for most programmers, and I think that's perfectly reasonable.

But

If you look at the examples in that article, the reason why the result enum doesn't work well for him is that the methods being called are primarily synchronous, and without return values. They aren't pure functions. This is imperative logic, not functional logic. Using return values either means checking the value after every call, or forces multiple consecutive lines of imperative logic to be turned into a chain of closures passed to then methods, which feels weird and restrictive when the lines themselves are independent of one another in every respect except execution order.

If this was Haskell, we'd use the do keyword to let us write our functional-code-that's-really-imperative-code as if it was just regular imperative code (via the power of monads and a bit of syntax sugar), but that's a PitA to do in Swift because the syntax support isn't there and you'd have to build the monadic infrastructure yourself (people have tried this of course - needless to say, many custom operators are involved).

But does that mean result types are bad? No, they're pretty awesome for functions that actually return a result that we want to pass to the next function (possibly an unspecified amount of time later). They just aren't great for handling errors produced by procedures that don't return anything, and execute sequentially on the same thread, because they must be checked after every call, and that quickly becomes tedious.

The same is true of Objective-C's error pointers (which is why so many developers just pass NULL and forget about them). The mechanism by which the error is returned isn't that important - it's the mechanism by which it is handled on the calling side that matters, and that's the thing that Swift's exceptions improve on - by replacing endless, tedious if (error != nil) { return error } statements with try.

But try only works in this one situation, with sequential imperative statements. If Brad Larson was working on something like a network library instead of a robot controller, result types would work much better for error propagation than Swift 2's exceptions, because exception-style errors don't really work for asynchronous, callback-based APIs. Consider the following example:

enum Result<T, U> {
  case Success(T)
  case Failure(U)
}

sendRequest(url: NSURL, callback: { result: Result in 
  switch result {
    case let .Success(result): // handle success
    case let .Failure(error): // handle error
  }
}

Internally, sendRequest may be doing something like

sendRequest(url: NSURL, callback: callback) {
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
    var result, error;
    // do something that takes a long time and either yields a result or an error
    if (result) {
      dispatch_async(dispatch_get_main_queue()) {
        callback(.Success(result))
      }
    } else {
      dispatch_async(dispatch_get_main_queue()) {
        callback(.Failure(error))
      }
    }
  }
}

How would this work using exceptions? If we tried to throw inside our dispatch block we'd be stuck because it would be GCD that receives the error, not our calling function (which has already exited by this point), nor our callback function (which will never be called). There's nowhere we can usefully put our try or catch.

And wrapping up these callbacks in a promise-based API makes no difference either; If our sendRequest function returns a promise instead of accepting a callback, we still need the errors to be handled internally by sending the error as an argument to a block/closure, not returning it from a function where it has been thrown. We can't return an exception instead of a promise, because at the point of return we don't yet know if the request has succeeded or not.

Result types and traditional Objective-C errors can be used either as return values or as callback arguments, which makes them suitable for both synchronous and asynchronous logic. Exceptions are just a special type of return value, and although they could be passed as a function argument, the try/catch semantics for using them can only be applied if they are thrown (aka returned) from a function, not passed to one.

  1. A Solution Tailored for Swift

So if Swift's not-really-new not-really-exceptions work nicely for synchronous, imperative code, and result enums work well for pure functions and async callbacks or promises, we're all good, right? The system works.

Nope.

It's gross that we have two totally different ways to handle errors based on whether the following line needs to use the output from the previous line, and whether it's called immediately, or after an async calculation. And Swift’s try/catch semantics do nothing to help reduce the error handling boilerplate needed inside callbacks.

The basic problem that exceptions (including Swift's) solve is that they change the default behavior of a function return. Normally, if a function returns, execution moves to the next line. But when the return value is an exception, we want execution to skip to the next catch statement, or to return the exception from the calling function so it can be handled by the caller's caller instead (and so on).

We could just make a rule that if a function returns an ErrorType, and the caller doesn't do something with it, we return from the function instead of continuing execution. I quite like this solution, as it avoids the need for throw, throws, do, try and catch, but it may be a little bit too magical.

So maybe we keep the try keyword, but change the meaning to "if this code returns anything other than an ErrorType, do nothing, otherwise bump us out of the current function and return the error". That would look like this:

func doSomethingThatMayFail() -> ErrorType? {
  ...
}

func doSomeThings() -> ErrorType? {
  try doSomethingThatMayFail() // if function returns nil, continue, otherwise return error
  doTheNextThing()
}

The doSomethingThatMayFail function doesn't return anything normally, but will return an error if it fails, as indicated by the ErrorType? return value. This is equivalent to writing func doSomethingThatMayFail() throws in Swift 2, but without needing a new keyword.

There would be two ways to call doSomethingThatMayFail() - we could either explicitly store its return value, or we could use try. Using try would be functionally equivalent to this:

func doSomeThings() -> ErrorType? {
  if let error = doSomethingThatMayFail() {
    return error
  }
  doTheNextThing()
}

Ignoring the result completely would be a compile-time error (or maybe just a warning, so it doesn't slow down prototyping). Using try inside a function that doesn't return an ErrorType would also be a compile-time error/warning.

There's probably no need for a catch keyword - if we want to handle the error, we just skip the try and handle the error value explicitly. Otherwise, we let it bubble up until we get to a function that does need to handle it.

What about functions that return a value on success? How would we use those with try? Well, these would need to return an enum of the form:

enum Result<T: Any, U: ErrorType> {
  case Success(T):
  case Failure(U)
}

And try would then decompose this into just the success value if the function didn't return an error, like this:

let foo = try functionThatReturnsFooOrAnError()

Doing that decomposition in the general case would be tricky, as it would involve slicing the error condition from an enum to form a new type. But if Result was a standard construct, like Optional, it would be much simpler. Instead of an arbitrary enum of "ErrorType + whatever", functions would have to return either a Result or an ErrorType? to be used with try.

(Aside: I'm abusing the ? modifier here to mean "or void" instead of "or nil", but the more I think about it, those are really the same thing. In an ideal version of Swift, I don't see a need for nil and void to be distinct concepts at all. A benefit of this would be that functions that return ErrorType? wouldn't have to explicitly return nil on success, they'd just use naked return, or reach the closing } without returning, implying success.)

So what about asynchronous callbacks - does this new definition of try help us there? Or is it no better than Swift 2's existing exception system (other than requiring fewer keywords)?

Well, if we examine how we're using try to decompose Result into ErrorType and a legitimate value, we can see a parallel with how if let or guard let is used to decompose Optionals.

Just as guard let x = x takes an Optional and either gives us a non-nil value or breaks out of the execution context, try does the same thing for Result. So for a callback, we could use the same kind of logic:

sendRequest(url: NSURL, callback: { result: Result in 
  let value = try result // decomposes Result into value/error, returns if error 
  // do something with result
}

This approach would require that our callback has a return type of ErrorType? instead of void, but it means we can use the same shortcut in our async callback as we can in our synchronous functions - bail at the first error and notify the caller that we've done so.

Since the semantics of try are similar to guard, we could even support an optional else, so that it mirrors the guard syntax:

let value = try result else {
  // clean up
  return result // at this point result is known to be an error
}

In which case we avoid the requirement that we must return an ErrorType? from our callback. We could instead forward the error to another function, for example:

let value = try result else {
  callErrorHandler(result)
  return
}

So, with a single try keyword, coupled to the established result enum pattern, we can support the same error handling approach introduced in Swift 2, with the same level of clarity, but with less boilerplate, and we can apply the same solution to async callback-based functions or promises.

Conclusions

In this post I've addressed why I believe Swift 2 introduced exceptions instead of result enums as its core error handling mechanism, and proposed an alternative that I think would better serve both its functional and imperative use cases.

Of course, I don't anticipate that the Swift language designers will read this and do away with the throw, throws and catch keywords. The purpose of this is mostly a thought experiment into how a hybrid functional/imperative language such as Swift might handle errors elegantly.

But hopefully some budding language designer out there may find some food for thought here when pondering the error mechanism for their new language.

@alexkarpenko
Copy link

Your proposed solution is exactly how error handling is done in Rust: https://doc.rust-lang.org/stable/book/error-handling.html

@anandabits
Copy link

I have also thought that the syntactic sugar for Swift's error handling should be built on top of a standard library Result type (following Optional) for precisely the reason you articulate. This will be useful at times even if Swift eventually adds something like async / await in a way that integrates well with the current error handling syntax.

One issue I have with almost all languages is that it is left up to documentation to articulate what errors a function or method can produce. Sadly the documentation is rarely complete or even close to complete. And without type information the compiler can't perform exhaustive ness checking very well. Unfortunately Swift's error handling system does nothing to address these issues.

Rust's error handling is very nice. One of the best parts is how it has sugar for automatically translating error types when crossing module boundaries. I hope Swift will eventually learn from this feature of Rust.

@amberstar
Copy link

Have you considered a completion handler that excepts a function that throws or returns a value:
Example of a completion handler that can throw: https://gist.github.com/amberstar/7861aee759b5d363d316

@mitjase
Copy link

mitjase commented Jul 7, 2015

They could modified the Optional, something like that would be more beautiful and readable to me.

enum OptionalWithError<T> {
    case None
    case Error     
    case Some(T)
}

And we really needed a do block, really, I personally could live without it. Thanks for the try! I will use it as often as possible ;) Try could work similar to guard.

try let x = doSomething() else {
    let error = x!!
    ...
}

@timjs
Copy link

timjs commented Jul 8, 2015

How about letting the API choose what to do on succes and failure? I think the desired solution here is passing two callbacks, one for success and one for failure, without doing an explicit switch on the input argument:

func sendRequest(url: String, onSuccess: Int -> (), onFailure: ErrorType -> ()) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
        var value: Int? // Calculate
        var error: ErrorType // Calculate
        if let value = value {
            dispatch_async(dispatch_get_main_queue()) {
                onSuccess(value)
            }
        } else {
            dispatch_async(dispatch_get_main_queue()) {
                onFailure(error)
            }
        }
    }
}

let someURL = "http://..."

sendRequest(someURL,
    onSuccess: { value in
        print(value)
    },
    onFailure: { error in
        print(error)
})

@frankus
Copy link

frankus commented Aug 21, 2015

Apologies in advance for derailing, but you shouldn't be checking error pointers in Objective-C:

When dealing with errors passed by reference, it’s important to test the return value of the method to see whether an error occurred, as shown above. Don’t just test to see whether the error pointer was set to point to an error.

(from Apple's Dealing with Errors documentation)

@uchuugaka
Copy link

I have to concur with frankus. The standard Cocoa pattern, unless the docs say otherwise for the method, is to check the return value for nil if object return or NO for BOOL return, in the nil or NO condition, check the outError, in the non-nil or YES condition, the outError is meaningless and should not be checked.

I can imagine if you are using a more pure old-school Objective-C style, all methods return id type by default, so you could check if (retVal) { if ([retVal isKindOf:[NSError class]]) { /* handle error /} else { / you got some other object back if you care. */} }

@couchdeveloper
Copy link

Both Result and Swift exception handling via throws syntax have their use cases. A Result<T> which is an Either<ErrorType, T> is useful to propagate errors from an asynchronous function to the call-site. Throwing an error is a convenient way to propagate an error from a synchronous function to its caller.

We can even use both approaches, which not only enhances usability, but which also looks quite nice and concise. Suppose there's an API for a Scala-like "futures and promises" implementation:

Without Swift's exception handling via throw

class Future<T> {

    func map<U>(f: T -> U) -> Future<U>` {
        ...
    }

}

The mapping function f does not throw - and returns the mapped value. How would we indicate a failure? We can't, since the return type is U and NOT Result<U>. IMHO, this API is severely limited.

Now, with throws enabled:

class Future<T> {

    func map<U>(f: T throws -> U) -> Future<U>` {
        ...
    }

}

Now, we are able to let the mapping function fail via throwing an error. The underlying implementation of course maps the error into a corresponding Result<U> which contains the error and propagates it from the asynchronous function to the call-site.

I've implemented this in my Scala-like Future and Promise Library (https://github.com/couchdeveloper/FutureLib).

Now, you might think, throwing, catching and mapping to a Result adds up a lot of boiler-plate code. Not at all. Properly handling the exceptions and mapping them to a corresponding Result does not really add much complexity to the code. You would probably not even recognize it in the sources:

    public final func map<U>(f: ValueType throws -> U) -> Future<U> {
        let returnedFuture = Future<U>()
        onComplete { [weak returnedFuture] result in
            let r: Result<U> = result.map(f)    // <- here, the throwing function's result is mapped to Result accordingly.
            returnedFuture?.complete(r)
        }
        return returnedFuture
    }

@barrkel
Copy link

barrkel commented Feb 13, 2016

Swift's errors have almost the exact same syntax and semantics as Java's checked exceptions (bar some tinkering with the way try is used) and are exactly as cumbersome to use, but without the performance costs that Java inherited from its earlier, unchecked behavior

I had a bit of a double-take when I read these lines, because exceptions using stack unwinding are much faster in the non-error case than explicit error passing. When you pass errors around, you (a) increase register pressure on codegen and often cause an extra parameter to spill to the stack, (b) increase code cache pressure from the bloat of inlined error checking, and (c) add stress to branch prediction from all the extra code paths being generated to check (or pattern-match) for errors after every return.

@timgcarlson
Copy link

Other than stating that I've been happy designing my APIs with callbacks for success and failure, I have nothing important to add other than an appreciation for this play on words (intentional or not)...

A Solution Tailored for Swift

@davebang
Copy link

davebang commented Mar 3, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment