Swift v2.0 Error Handling – Revisit

Last night I posted my initial thoughts about Swift's approach to error handling. I've softened a little to the approach as I got to play with it a bunch this morning, including re-visting what a Result<T, E> looks like in Swift.

It's only after going through that excercise again that reminded me just how clunky dealing with enum values are, especially those that contain values, that I can appreciate why the Swift team went in the direction they did.

enum MyCustomError: ErrorType {
    case Happy
    case Dance
}

enum Result<T, E> {
    case Ok(T)
    case Error(E)
}

func base() -> Result<(), MyCustomError> {
    return Result.Error(MyCustomError.Dance)
}

func handle() {
    let result = base()

    switch (result) {
    case .Ok(_):
        println("required...")

    case let .Error(error):
        switch (error) {
        case MyCustomError.Happy:
            print("Happy error")

        case MyCustomError.Dance:
            print("Dance error")
        }
    }
}

Or, with the Swift error handling approach.

enum MyCustomError: ErrorType {
    case Happy
    case Dance
}

func base() throws {
    throw MyCustomError.Dance
}

func handle() {
    do {
        try base()
    }
    catch MyCustomError.Happy {
        print("Happy error")
    }
    catch MyCustomError.Dance {
        print("Dance error")
    }
    catch {
        print("catch all, because no types")
    }
}

Of course… those aren't our only options though. See, there was this lovely little keyword guard that was also introduced to us. So, by putting some better smarts into Result<T, E>, we can get something that looks like this:

enum MyCustomError: ErrorType {
    case Happy
    case Dance
}

enum Result<T, E> {
    case Ok(T)
    case Err(E)

    // what if these were simply generated for all enums?
    var ok: T? {
        switch self {
        case let .Ok(value): return value
        case .Err(_): return nil
        }
    }

    var err: E? {
        switch self {
        case .Ok(_): return nil
        case let .Err(e): return e
        }
    }
}

func base() -> Result<(), MyCustomError> {
    return Result.Err(MyCustomError.Dance)
}

func handle() {
    let result = base()

    guard let value = result.ok else {
        switch result.err! {
        case MyCustomError.Happy:
            print("Happy error")

        case MyCustomError.Dance:
            print("Dance error")
        }

        return
    }

    print("value: \(value)")
}

Swift's throws approach is still more terse, but provides no type-safety on the error type. It might be possible to stream-line the approach above some more, but it feels better to me. I never liked exceptions used as code flow control and the do-try-catch approach seems to just beg for that.

Eh… I'll keep noodling.

Swift v2.0 Error Handling – Revisit

Swift v2 – Error Handling First Impressions

UPDATE June 9th, 2015: Go watch the 'What's New in Swift' talk. It answered nearly all of my concerns below.

Swift v2.0… honestly, I was a bit surprised by many of the updates to the language, especially the error handling. There's a session on Tuesday to talk about many of the changes in depth, but I'm pretty mixed about the update.

The worst update, in my opinion, is the error handling. It's basically a pseudo-exception system.

enum SomeException: ErrorType {
    case Oops
}

func bad() throws {
    throw SomeException.Oops
}

func good() {
    do {
        try bad()
    }
    catch SomeException.Oops {
        print("There was an exception")
    }
    catch {
        print("This is required because...")
    }
}

Ok, so what do we have up there? Well, we have a function bad that is marked as throwing an exception, and we have a function good that makes use of calling that function and handling the exception.

Already I'm seeing problems that I don't know if can even be solved.

  1. No type information about the type of errors that can happen from bad. This strikes me as extremely odd from a language that is all about type-safety.
  2. A catch-all catch statement is required – I don't know if Swift can even fix as it doesn't seem to have enough information to determine all possible types to be thrown, especially for deep function calls.
  3. Verbose with needing both do to scope your catch calls with and try to prefix the call to functions that can throw.

If you want to simply pass the buck on the error, that's pretty simple to do:

func good() throws {
    try bad()
}

Now… this is where things get interesting. There is a whole section about stopping the error propogation. If you know (famous last words…) that the function you are calling will not throw, you can do this:

func goodish() {
    try! bad()
}

This is the rough equivalent of force-unwrapping your optionals. This is not great, in my opinion. The really bad part is that if you use code that does this, there is no way for you handle the exception that it can throw.

func goodish() {
    try! bad()
}

func nogood() {
    do {
        try goodish()
    }
    catch {
        print("sorry, never going to happen")
    }
}

Let's say that goodish actually does throw in an edge-case that the programmer missed. Well, if that's the case, too bad for you; I sure hope you have access to that code so you can fix it. In fact, the code above will issue you a compiler warning because goodish() isn't marked as throws.

Ok, so you get the idea to try this:

func better() throws {
    try! bad()
}

func nogood() {
    do {
        try better()
    }
    catch {
        print("sorry, never going to happen")
    }
}

Nope… still going to get a runtime error.

The reason? I'm speculating here, but it seems like Swift is simply adding a bunch of compiler magic to inline the error information as a parameter to the function. That information is thrown away when you call try! and will not be propogated out. The nice benefit of this is you can get an error-handling system that has great runtime performance. However, the downside, is that you've built a system that codifies that correctness is up to the programmer now and not verifiable by the compiler as much as it could have been.

I would have much rather seen a system that codifies the error as the return type and forces handling of it, as in Rust. I thought that was the direction we were heading with Optional<T> when it was introduced with Swift 1.0.

Who knows, maybe it will grow on me, but I think I may be sticking with the Result<T, U> pattern.

Swift v2 – Error Handling First Impressions