Error Handling in Swift 2.0

Swift 2.0 introduces pretty dramatic changes to the way we handle errors. New keywords, new patterns - there’s a lot to process. I haven’t quite decided how I feel about the new error handling API, but for the moment I’m enjoying the new, shiny parts of Swift 2.0 the simple facts of their newness and shinyness.

How It Used To Be

In Swift 1.2, a common pattern for handling errors is constructing a Result enum containing two associated values: an NSError or a value of type T.

enum Result<T> {
    case Error(NSError)
    case Result(T)
}

Using Result to handle the response from a network request might look like this:

func makeNetworkRequest<T>(request: () -> T?, callBack: (Result<T>) -> ()) -> Void {
    let t: T? = request()
    let error: NSError = NSError(domain: "errorDomain", code: 0, userInfo: [NSLocalizedDescriptionKey: "egregious error"])

    if let result = t {
        callBack(.Result(result))
    }
    else {
        callBack(.Error(error))
    }
}

An NSError object is passed to the failure callback. The caller can inspect the NSError to get more detailed information about the error and execute code accordingly. As far as error handling goes, this pattern is largely inherited from Objective-C. There is nothing particularly “Swifty” about it.

The Swift 2.0 Way

In Swift 2.0, we can specify different kinds of errors using an enum:

enum MyError: ErrorType {
    case FantasticError
    case OkError
    case TerribleError
}

Our new error type conforms to ErrorType, which is actually just an empty protocol that helps us classify things as errors.

Swift 2.0 requires that we add the throws keyword to the function signature of any function that might throw an ErrorType. The function above takes an optional MyError parameter and, if it can be unwrapped, throws it:

func mightThrowAnError(error error: MyError?) throws -> Void {
    if let anError = error {
        throw anError
    }
}

There are a few things to be aware of here:

  1. In order to call any function that throws, we must preface it with the try keyword. this lets the compiler know that there’s a possiblity that an error will be thrown.
  2. For functions that invoke try, we can enclose everything in a do...catch statement. do ...catch uses pattern matching to capture the thrown error and execute code depending on its type.
  3. If we do wrap our try invocation in a do...catch statement, it must be exaustive for all of the possible error types that can be thrown. In our case, we have FantasticError, OkError, and TerribleError.
do {
    try mightThrowAnError(error: .FantasticError)
}
catch MyError.FantasticError {
    print("this error is fantastic")
}
catch MyError.OkError {
    print("this error is just ok")
}
catch MyError.TerribleError {
    print("this error is just ok")
}
catch {
    "this will throw a plain old exception"
}

If we run this code, the console will print out “this error is fantastic”.

Notice that in addition to the three possible MyError values, we have an additional catch statement tacked on at the end. We add this here because the compiler requires that we account for all possible error type.

Try?

In addition to try, Swift 2.0 gives ustry? as a convenient way to handle an error by converting it to an optional.

func mightThrowAnError(error error: MyError?) throws -> Int {
    if let anError = error {
        throw anError
    }
    return 1
}

let result = try? mightThrowAnError()

Instead of halting execution, if an error is thrown in the snippet above, result will simply be set to nil.

Try!

Try! feels a little strange to me, but here it is: if you are confident that a throwing function will not throw an error, you can disable error propagation by calling it with try!:

try! mightThrowAnError()

If mightThrowAnError() does indeed fail at runtime, you will simply get a runtime error.

Rethrows

In addition to throws, we get another new keyword that lets us pass an error from a function back to the caller: rethrows. rethrows takes a throwing function as an argument, and, like throws, it appears in the function signature before the return type.

func mightRethrow<T>(thrower: () throws -> T) rethrows -> Void {
    try thrower()
}

Defer

Swift 2.0 also adds a new keyword to specify cleanup actions. To defer the execution of code until the current scope has been exited, wrap it in a defer statement:

func mightThrowAnErrorButWillAlwaysExecuteDeferStatement() throws -> Void {
  defer {
    print("executing defer statement")
  }
  do {
    try mightThrowAnError(error: .TerribleError)
  }
  catch {
    ...
  }
}

Regardless of whether the function exits the current scope because of an error or for another reason, the defer statement will always execute.

Everything considered, my first impressions of Swift’s error handling API are positive. Syntactically, some aspects don’t feel entirely natural (appending the throws keyword to the signatures of throwing functions, for instance). Still, I love the flexibility that custom error types give me when it comes to error handling. Swift 2.0’s model for error handling may take some getting used to, but my belief is that we can look forward to writing cleaner, more maintainable error handling code with it.