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 throw
ing 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 Array
2 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. ↩︎