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