Figure

A blog about Swift and iOS development.

Swift Exceptions are Swifty: Part 2

Here's what actually handling an error looks like in Swift 2:1

func human() throws -> String{ ... }
do{
  let h = try human()
  //do stuff with «h»
} catch{
  print("Error: \(error)")
}

In Part 1, we discussed how the underpinnings of throws and throw are, at least conceptually, more Swifty than they first appear. Like a surprising number of types, conditionals, and other "language features" of Swift, throws and throw feel like they could be implemented in the standard library. There's some syntactic sugar, yes, but not a lot of magic.

What about actually handling errors, though? Is it even possible to implement try/catch in terms of Swift, or is it something that requires new alien flow control bolted on to the runtime?

There are fewer clues to go on, here, but it's illuminating to look at the other enhancements added to Swift 2 along side error handling. In doing so, we may discover that neither try nor catch are as magic as they first appear.

Eyes Up, Guardian

For example, let's look at guard. It's whole purpose is to evaluate a condition in the current scope and, if the condition doesn't hold true, return out of said scope. That is to say:

guard let x = foo() else{
  //must return!
}
// can do stuff with «x».

There are a number of ways this kind of early exit can simplify code all on its own, of course. But looking at our error handling above, it seems like the functionality behind guard might have another use.

Error handling in Swift 2, remember, is performed within an explicit new scope created by the do statement. Functions that can throw are evaluated in this scope and, if an error is found, must exit immediately. Sound familiar? try is essentially acting as a guard:

do{
  let h = try human()
  //do something with «h»
}
// conceptually equivalent to:
do{
  guard /*human doesn't error*/ else{ return }
  //do something with «h»
}

Case Study

There's one fly in the ointment. As we mentioned last week, human() is returning an Either enumeration that contains either a value or an ErrorType. We need to be able to unwrap that enumeration to figure out if we've erred or not.

Classically, the only way to unwrap enums like this was with a switch statement:

enum Either<T,U>{
  case This(T)
  case That(U)
}

switch myEither{
case .This(let str):
  print(str)
case .That(let error):
  //handle error
}

But Swift 2 has added the ability to add case clauses to a number of statements outside of switch. Statements like if, for...in, and — of most interest to our current conversation — guard:

func human() throws -> String
do{
  let h = try human()
  //do something with «h»
}
// conceptually equivalent to:
func human() -> Either<String,ErrorType>
do{
  guard case .This(let h) = human() else{
    return
  }
  //do something with «h»
}

Deferential Treatment

Another new addition to Swift 2 is the defer statement. It lets you declare some code that gets executed immediately before control exits the current scope:

func blogPost(){
  defer{
    print("deus ex machina!")
  }
  print("get too clever with examples")
  print("paint self into corner")
}

blogPost()
//> get too clever with examples
//> paint self into corner
//> deus ex machina!

The scope that triggers a defer could be anything. An if statement. The case of a switch… or even a do:

func blogPost(){
  do{
    defer{
      print("deus ex machina!")
    }
    print("paint self into corner")
  }
  print("still have to write blog")
}

blogPost()
//> paint self into corner
//> deus ex machina!
//> still have to write blog

Interesting! Now let's take another look at catch:

do{
  try human()
  print("vegetable")
  print("mineral")
} catch{
  print("error")
}

Assuming human() throws an error, we'll never see "vegetable" or "mineral" printed. That's because as soon as human() fails, we exit the scope of the do we're in. So how does the catch get called?

Is sounds rather like a defer, doesn't it? If we replace that try with a guard, and the catch with a defer, we get essentially the same behavior:

do{
  defer{
    print("error")
  }
  guard case .This(let h) = human() else{
    return
  }
  print("vegetable")
  print("mineral")
}

Building try/catch

Using this knowledge, can we build our own try/catch? If we assume that throwing functions return an Either<T,ErrorType>, the answer is yes! Though it's not quite as pretty as what we've seen so far. We'll need a temporary variable to hold the error state, for example. And because defer will capture it, it'll need to be optional. In the end, something as simple as:

do{
  let h = try human()
  print(h)
} catch{
  print(error)
}

might look like this monstrosity by the time we're finished coding it by hand:

do{
  var tmp:Either<String,ErrorType>?
  defer{
    if case .That(let error) = tmp!{
      print(error)
    }
  }
  tmp = human()
  guard case .This(let string) = tmp! else{
    return
  }
  print(string)
}

So I, for one, welcome our syntactic sugar overlords and pray we never need write the likes of this again.

But the point, of course, is not that we need to do any of this. It is that we can. The mechanisms for handling errors, buried deep in the primitive bellies of most languages, feel exceptionally close to the surface of Swift. Concepts that are called "magic" in C++ or "you-just-have-to-learn-it" in Java are knowable, buildable things in Swift. This extends from ARC through Int and Array2 apparently all the way up to try/catch.

More and more it's starting to feel like this is what it means to be "Swifty".


1: I'm willfully simplifying my example by only talking about functions, not methods, and ignoring pattern-matching in the catch clause. Everything discussed should apply, regardless. ↩︎

2: Somewhere, there's a 70,000 word blog post waiting to be written on the wonders of isUniquelyReferenced. UPDATE: Of course it's already written, and of course it's by Mike Ash. ↩︎


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