A blog about Swift and iOS development.

Swift Exceptions are Swifty: Part 1

There's a lot of sturm and drang around the announcement of exceptions in Swift 2. I get it. I fought in the Java wars. I know the terror (or worse: apathy) try/catch blocks can inspire.

But remember that first time we ⌘-clicked on Int and realized "My god, it's written in Swift"? The same experience awaits us if we dig a little into the design of exceptions. They may look like a scary language-level feature, but the details are elegant. Swift 2's exceptions feel written in Swift, not ossified runtime magic. This makes them more dynamic than they might first appear (and holds the door open for more generalized forms of exception handling in the future).

throws and throw

There's no greater example of this than marking a function with throws:

func infallible(val:Int) -> String{
  return "Perfect Things"

func human(val:Int) throws -> String{
  if mistake{
    throw MyErrorType
  return "Mostly Perfect"

At first blush, really weird things are going on here. infallible() is functional. It operates on inputs and returns its output. Simple.

But human() takes input, returns output — and has some third kind of dimension introduced by this throws keyword. It's no longer an in-and-out operation. In a way, throws appears to break the very definition of what a function is.

Fans of monadic error handling often point out that, by wrapping errors into the return via an Either type,⁠1 the purity of the function is maintained:

func human(val:Int) -> Either<String,NSError>{
  if mistake{ return Either(error) }
  return Either("Mostly Perfect")

And while this is true, the costs are obvious. Angle brackets, monads, and the cognitive overhead of wrapping and unwrapping values for both implementors and callers, alike.

An ideal solution might maintain functional purity under the covers, forcing functions to error out by returning Either. But then offer enough syntactic sugar on top to save the uninitiated (or initiated-but-uninclined) from having to reason about functional concepts like functors, binds, and what maps on what.

Would it blow your mind to learn this is essentially what Swift 2 does?⁠2

It's telling that exceptions have arrived in the language alongside multi-payload enums. This makes it trivial to create Either types without the awkward, inefficient boxing of values previously required. And it would seem exceptions take advantage of this. throws is just syntactic sugar. The following are, at least conceptually, identical:⁠3

func canFail<T>() throws -> T{}
//Conceptually equivalent to:
func canFail<T>() -> Either<T,ErrorType>{}

Which makes throw a simple return with an Either wrapper:

throw MyErrorType
//Conceptually equivalent to:
return Either<MyErrorType>

Note that even if we don't understand Either, or if generics make us break into a cold sweat, or if our knuckles turn white at the mere mention of "monads", throws, throw (and all the try/catch machinery we'll get to next week) nicely isolate this all away from us.

This is in keeping with Swift's founding principals. Back during the introduction of access modifiers, Chris Lattner told us one of Swift's design goals is to allow features to be introduced to new users incrementally.

He calls out Java as being particularly bad at this, requiring us to understand public static void main before we can write a single line of code.⁠4 Now imagine if, when learning Swift, we had to understand monads before we could handle exceptions. Our heads would never stop spinning!

So isolating away Either is a good thing for beginners. But, while the new user can eschew all access control while learning Swift, public, internal, and private are waiting there for the advanced practitioner. What about Either? Can we opt-out of the sugar Swift 2 bakes on top of exception handling and manually deal with Either?

Short answer: no. But patience is a virtue. Remember, access control didn't make it into Swift 1 until beta 4. Who knows what future betas of Swift 2 will bring?⁠5

1: See also: the Result monad, which is essentially an Either<T,U> with U predefined as an error type.↩︎

2: "Under the covers errors propagate a lot like Result<T>. There's no dynamic unwinding like exceptions." –Joe Groff ↩︎

3: "throws -> T is like -> T | ErrorType. If you don't catch an error, you implicitly return it." –Joe Groff ↩︎

4: And if that single line can throw, we have to add understanding checked exceptions to the list. ↩︎

5: "Didn't make it for WWDC, but we plan on having standard funcs for wrapping and releasing errors into an Either."–Joe Groff ↩︎

Hit me up on twitter (@jemmons) to continue the conversation.