Error Handling in Swift: Might and Magic
If distance brings perspective, proximity brings understanding. Concepts that were remote and downright strange when I played with them in Haskell or read about them in Scala are blindingly obvious solutions to any number of problems now that I am programming in Swift.
Take error handling. As a concrete example, dividing two numbers, which will fail if the divisor is zero. This is how I would handle this in Objective-C:
NSError *err = nil;
CGFloat result = [NMArithmetic divide:2.5 by:3.0 error:&err];
if (err) {
NSLog(@"%@", err)
} else {
[NMArithmetic doSomethingWithResult:result]
}
By now, this practically seems like the natural way of doing things; I forget the convolutions I’m going through and how little they correspond to what I really want the program to do:
Fetch me something. If it fails, let me know so I can handle it.
I’m passing parameters, dereferencing pointers, returning things every time, and ignoring the return value some of the time. This is very disorganized code, really, for a number of reasons:
- I am talking the machine’s language—pointers and dereferenced pointers.
- I have to provide the method with the means to inform me of errors.
- The method returns a result irrespective of success or failure.
Each of these is a potential source of bugs, and each can be addressed in Swift in a more systematic manner. The first one, obviously, is a non-issue since Swift abstracts away the pointers. The way to get around the other two problems is enumerations.
• • • • •
Error-prone computations have two possible outcomes:
- Success, with some type of resulting value.
- Failure, hopefully with a message explaining the failure.
These are mutually exclusive—in our example, dividing by zero leads to an error, and everything else to success. Swift conceptualizes mutual exclusion by supporting enumerations, usually called “enums”. Here is an enum that defines the result of an error-prone computation:
enum Result<T> {
case Success(T)
case Failure(String)
}
An instance of this type is either a Success
with an associated value or a Failure
with a message describing the cause. Each case
defines a constructor: the first one takes an instance of T
as its parameter (the result value), and the second one a String
(the error message). The Objective-C code snippet becomes, in Swift:
var result = divide(2.5, by:3)
switch result {
case Success(let quotient):
doSomethingWithResult(quotient)
case Failure(let errString):
println(errString)
}
All in all, a bit wordier but much better! The switch statement lets me name the constructor arguments (quotient
and errString
) and access them in my code, and I can then process the result based on the outcome. All the issues are addressed:
- No explicit pointers, much less dereferenced ones.
- No need to pass extraneous parameters to
divide()
. - Compiler-enforced handling of all cases in the enum.
- Since
quotient
anderrString
are wrapped by the enum, they can only be processed in their respective case statements—it’s impossible to process a result in an failure state.
Most importantly, the code expresses exactly what I intended: computing something and handling any errors. It maps directly to my concept of the task.
• • • • •
Now let’s consider a richer example. What if I want to process the result? Let’s say I want a function that will take a division result and compute a magic number from it by finding the least prime factor of the quotient and taking the logarithm. (There is nothing magic about the computation; I just picked operations at random.) The code would look like this:
func magicNumber(divisionResult:Result<Float>) -> Result<Float>
switch divisionResult {
case Success(let quotient):
let leastPrimeFactor = leastPrimeFactor(quotient)
let logarithm = log(leastPrimeFactor)
return Result.Success(logarithm)
case Failure(let errString):
return Result.Failure(errString)
}
}
Simple enough. But now that I have my magic number, I want to get the magic spell it corresponds to. So I write it up like this:
func magicSpell(magicNumResult:Result<Float>) -> Result<String>
switch magicNumResult {
case Success(let value):
let spellID = spellIdentifier(value)
let spell = incantation(spellID)
return Result.Success(spell)
case Failure(let errString):
return Result.Failure(errString)
}
}
Now, however, I have a switch statement in each function, and they look identical. Moreover, those methods really only want to run computations on the success value. The error handling a repetitive distraction.
Whenever things are repetitive, it pays to look for an abstraction. Again, Swift has the tools. Enums in Swift can have methods, and I can remove the need for these switch statements by creating the following map
method on the Result
enum:
enum Result<T> {
case Success(T)
case Failure(String)
func map<P>(f: T -> P) -> Result<P> {
switch self {
case Success(let value):
return .Success(f(value))
case Failure(let errString):
return .Failure(errString)
}
}
}
The map
method, so named because it maps Result<T>
s to Result<P>
s, is straightforward:
- If there is a value, it applies
f
to it and wraps the new value in aResult.Success
, - If there is a failure, it returns the failure.
Yet simple though it is, map
allows downright sorcery. Because map
handles the error states, I can rewrite magicNumber
and magicSpell
strictly in terms of the primitive operations:
func magicNumber(quotient:Float) -> Float {
let lpf = leastPrimeFactor(quotient)
return log(lpf)
}
func magicSpell(magicNumber:Float) {
var spellID = spellIdentifier(magicNumber)
return incantation(spellID)
}
Which then lets me get the magic spell like so:
let theMagicSpell = divide(2.5, by:3).map(magicNumber)
.map(magicSpell)
Or, if I don’t need those methods in the first place:
let theMagicSpell = divide(2.5, by:3).map(findLeastPrimeFactor)
.map(log)
.map(spellIdentifier)
.map(incantation)
This is tremendous. The need for error handling during computations is abstracted away. All I need to do is specify what computations I want to apply, and map
will take care of propagating any errors.
This doesn’t mean that I will never have to write a switch statement. At some point I will need to either output the error or output the final result of the chained computations. But that is one switch statement, at the very end of processing—no need to write intermediate methods that process the Result
types; those need only concern themselves the success values.
Sorcery, I say!
• • • • •
None of this is academic. Abstracting away error-handling is very useful when dealing with transformations of data: how often do we request data from a server that returns a JSON string or an error, and then want to transform the string into a dictionary, then map the dictionary into an object, which is in turn passed to the presentation layer which creates four display objects from it? The Result
enum allows all the transforming methods to be written as if the data they received were always valid, and for errors to still propagate via calls to map
.
If you haven’t seen this before, take a second to think about it. Play with the idea a little (once the code above compiles; the compiler still doesn’t handle the IR for generic enums). I think you’ll start appreciating the power.
• • • • •
If you’re into math, you may have noticed a bug in the example I gave. Logarithms are not defined for negative numbers and Float
s can be negative. Therefore log
would not in reality return a Float
, but a Result<Float>
. Passing it to map
would then result in nested Result
s which would break our nice plan of processing only primitives. Don’t worry, there’s a solution to this. You can probably come up with it on your own. For those who don’t care to, I will discuss it in [my next post][].
[my next post]: http://nomothetis.svbtle.com/error-handling-in-swift-part-ii