Swift 2 error handling in practice

Apple_Swift_Logo.png

This week, Apple enhanced Swift with several additions or changes, bumping it to version 2. Perhaps the biggest change was the introduction of an official error-handling model based on try/catch semantics. At first, I was skeptical about this approach when compared to other techniques (primarily the use of a Result<T,U> return value), but after applying it to real-world code, I've found it to work extremely well.

In some ways, this is a follow-on to an earlier article I wrote about why we decided to rewrite our company's robotics control software in Swift. A large part of the motivation for that rewrite was to increase the safety of our code, and proper error handling is a core element of that. In fact, a surprisingly high percentage of the code we've been able to clean up, remove, or make safer has been related to error handling.

For example, we have a robotic arm that we need to move. We want to command it to move up from a surface, move laterally, and then move back down to another point on a surface. We talk to this robot using serial communication over an RS-232 interface.

There are multiple points at which this can fail: corrupted communication over the serial bus, a dropped serial connection, the robot hitting an obstruction, the robot somehow traveling to the wrong position, power being cut, etc. We want to catch each of these possible errors, recover intelligently from the ones we can, and present the others to the user to help them correct whatever went wrong.

In review: Objective-C error handling

With an Objective-C codebase, the traditional way of approaching this was through the use of NSError** parameters and specific return values (BOOLs set to NO, nil values for objects, or other magic constants). Again, I've talked about the disadvantages of this in my previous post, but let's look at an example of how this worked in practice.

Let's say we wanted to command this robot to move up, over, then down, and to read, verify, and return the final coordinate it arrived at. For each step, we'll catch errors and abort early if one is received (we don't want to keep telling the robot to move somewhere when something's gone wrong). Errors will be bubbled up to the top level, or recovery attempts will be made for things that might be less severe.

In Objective-C, that code might look like the following:

- (Coordinate *)moveToCoordinate:(Coordinate *)targetCoordinate error:(NSError **)error;
{
	if (targetCoordinate == nil)
	{
		if (error != NULL)
		{
			*error = [self errorForRoboticsErrorCode:BADCOORDINATE];
		}
		return nil;
	}
 
	if (![self moveUp error:error])
	{
		return nil;
	}
	if (![self moveOverCoordinate:targetCoordinate error:error])
	{
		return nil;
	}
	if (![self moveDownToCoordinate:targetCoordinate error:error])
	{
		return nil;
	}
	Coordinate *newPosition = [self readCurrentCoordinate:error];
	if (newPosition == nil)
	{
		return nil;
	}
	if (!newPosition.isEqualTo(targetCoordinate))
	{
		if (error != NULL)
		{
			*error = [self errorForRoboticsErrorCode:MISMATCHEDPOSITION];
		}
		return nil;
	}
 
	return newPosition;
}

This is a method on a robotics class that both can take in and bubble up errors from lower-level operations (failures in serial communications, etc.) as well as creating and passing up an error if the final position the robot's motors report is different from our target.

There are a number of issues with the above code: a lot of noisy boilerplate code here for error handling, weakly-typed errors, requiring someone to remember the various error-handling conventions (is an error indicated by nil here? a NO value? something else?), and there's no compiler enforcement of error-handling. The latter is particularly troubling, since it's all too easy to forget to handle an error case, as we've done many, many times over the years.

That's all without mentioning the need to check for nil values in our input coordinate to avoid catastrophically sending our robot to (0,0,0), a bug that once cost us a lot of money, and the fact that we need to make sure error isn't nil to avoid a null-dereferencing crash.

Increasing safety and reducing code using Result<T,U>

Swift 1.0 gave us the tools to make a better way of handling errors: the Result<T,U> type. Swift allows for enums with associated values, and for generics, so we can write a Result type that can contain either a value (of any type) or an error (of any type). Such a type looks like the following:

public enum Result<T, U> {
    case Success(T)
    case Failure(U)
 
    func then<V>(nextOperation:T -> Result<V, U>) -> Result<V, U> {
        switch self {
            case let .Failure(error): return .Failure(error)
            case let .Success(value): return nextOperation(value)
        }
    }
}

This lets us enforce error handling at compile time by making sure you have to account for the error case before extracting a value from a potentially error-throwing function or method. Generics preserve type information for values being returned through this, and enum cases mean that something can only return a value or an error, never both.

The .then() method we've provided in the Result definition lets us avoid a lot of error-handling boilerplate and clean up our code. This works by taking in a closure that outputs another Result type. If the Result from the previous step was a .Failure, that .Failure is simply returned and execution of the chained calls stops at that point. Only if the previous step was a .Success is the value from that step unwrapped and passed into the next step in the process.

This is the same process used for optional chaining in Swift, only with the additional context of an error value that can be provided. Both of those are effectively monadic binds, for those familiar with these concepts in other languages.

Using a Result type, we can now rewrite the above Objective-C in Swift:

func moveToCoordinate(targetCoordinate:Coordinate) -> Result<Coordinate, RoboticsError> {
    return self.moveUp()
        .then{self.moveOverCoordinate(targetCoordinate)}
        .then{self.moveDownToCoordinate(targetCoordinate)}
        .then{self.readCurrentCoordinate()}
        .then{coordinate -> Result<Coordinate, RoboticsError> in
            if (coordinate != targetCoordinate) {
                return .Failure(.MismatchedPosition)
            } else {
                return .Success(coordinate)
            }
        }
}

The first thing you can see is how much more compact this is. We've gone from 38 lines to 12. To my eye, this is also a lot easier to read, as the error-handling boilerplate has been removed in order to focus on the actions themselves. The reason why I called our continuation operator .then() is to indicate sequencing of the commands we're sending.

Beyond the brevity of the above, the chaining of these Result values causes error handling to be enforced by the compiler. We don't have to figure out the different ways that an operation can express that it's handing back an error. We also can use our own simpler custom enums as error types (the RoboticsError enum I use in the above), rather than the generic bucket that is NSError.

Of course, Swift also lets us forget about checking for nils as inputs to non-optional values and eliminates any potential crashes around NSError double pointers, since those are no longer necessary.

We are just about done with our rewrite of our robotics control software from Objective-C to Swift, and our Swift version with identical functionality is roughly one-fourth the size of our Objective-C version, largely due to the improved error handling provided by Result<T,U>.

Swift 2 and the new try/catch error handling model

We aren't the only ones that think Result, or a type like it, is a solid solution for error handling in Swift, so it took me by surprise that the Swift team went in a seemingly different direction with Swift 2's new error model. That is, until I started working with it in my own code and found that it did almost everything my Result-based handling does and did it in a cleaner manner.

Swift 2 provides error handling by the means of several new keywords: do, try, catch, throw, and throws. To indicate that a function or method can provide an error, you mark it with throw. Errors cannot be thrown within functions or methods that lack this marking. Similarly, error-throwing functions or methods cannot be used without using the try keyword before them. All errors must either be caught (using do/catch), or passed upward (at which point the function or method must be marked as throws).

Multiple error-throwing functions can be called in sequence, but the first to throw an error aborts execution at that point. An error being thrown acts more like a return statement than the older-style Objective-C NSExceptions, and plays nice with ARC.

This compiler-enforced handling of errors (you must explicitly catch them at some point, or your code won't build), and the way error-throwing functions are marked, makes this all act in practice very much like the Result type described above. In fact, this provides some advantages over the Result type in Swift code.

If we look at our above Result-based Swift code, there are a few awkward things that stand out. First, following the flow of values through that code is a little weird. We have a return at the top of our method, rather than the bottom, because everything there is one long statement. A Result is returned from self.moveUp(), and then depending on the .Failure / .Success state of that self.moveOverCoordinate(targetCoordinate) will return a Result. This flows down the line until the final step, where the last Result is passed all the way back up to the return.

We have to use closures to chain these, since we aren't always passing the value returned from a previous step into the next. Swift's type system does a good job of inferring types all the way down, but it doesn't quite know what to do with the final step in our process, so we have to give it an explicit type signature. If these .then() statements get chained too long, the type inferrer can choke, too. If we screw up our code at some point in these chains, the error messages we get back can be extremely cryptic due to this type inference.

Also, methods like self.moveOverCoordinate(targetCoordinate) don't return a value, but we still have to have a return type of Result<(),RoboticsError> for them in order to let them participate in this error handling. We have to make sure they return a .Success() value if them complete, even though they shouldn't return anything.

Let's see what the above looks like in the new Swift error handling model:

func moveToCoordinate(targetCoordinate:Coordinate) throws -> Coordinate {
    try self.moveUp()
    try self.moveOverCoordinate(targetCoordinate)
    try self.moveDownToCoordinate(targetCoordinate)
    let coordinate = try self.readCurrentCoordinate()
    if (coordinate != targetCoordinate) {
        throw .MismatchedPosition
    }
    return coordinate
}

This has gotten even shorter (9 lines vs. 12), and become easier to read, while providing the exact same error safety and compile-time checks. Each operation can still kick out an error and abort early as needed, even the self.readCurrentCoordinate() operation before it returns a value in that let statement. That alone can make this error-handling code much easier to follow.

This new mechanism also provides other benefits over Result types in Swift code. While I have been saying that the compiler enforces error handling when returning a Result type, that isn't true for all cases. If you have a function where you are ignoring the value it returns (or it returns no value), it's still very easy to use that function without .then() or other checks and not handle errors from it. With Swift's new error-handling method, any function that throws an error must be marked, and that error must be handled or thrown, or the compiler will stop you right then.

Functions that return no value, but take an action that can provide an error, can safely go without a return statement. There's no need for a Result<(),RoboticsError>.

Other procedural logic, like loops, can be a pain to use Result-returning functions within. Swift's try statements make that easy to handle, with loops being bailed out of naturally in the case of an error.

Using non-Result-returning functions or methods can be a pain with Result logic. Let's say I wanted to inject a delay before moving from one point to another in the above code. With the Result logic, that would look something like this:

return self.moveUp()
    .then{self.moveOverCoordinate(targetCoordinate)}
	.then{NSThread.sleepForTimeInterval(2.0)
        return .Success(Box())
    }
    .then{self.moveDownToCoordinate(targetCoordinate)}

The artificial Result is returned just to keep the train going. With try-based error handling, that becomes the more natural:

try self.moveUp()
try self.moveOverCoordinate(targetCoordinate)
NSThread.sleepForTimeInterval(2.0)
try self.moveDownToCoordinate(targetCoordinate)

Likewise, if there are values that need to be accumulated throughout a string of operations and then synthesized at the end, Result tends to make you use a var for those (to allow for modification within a closure):

var xComponent:Double = 0
var yComponent:Double = 0
var zComponent:Double = 0
let result = readXComponent()
    .then{value -> Result<(), CommunicationsError> in
        xComponent = value
        return .Success(Box())
    }
    .then{readYComponent()}
    .then{value -> Result<(), CommunicationsError> in
        yComponent = value
        return .Success(Box())
    }
    .then{readZComponent()}
    .then{value -> Result<Coordinate, CommunicationsError> in
        zComponent = value
        return .Success((xComponent, yComponent, zComponent))
    }

As compared to the let values you can use with try-based error handling:

let xComponent = try readXComponent()
let yComponent = try readYComponent()
let zComponent = try readZComponent()
return (xComponent, yComponent, zComponent)

As a last sidenote, I was surprised to find out that the compiler will automatically translate the signature of many Objective-C methods that use standard NSError** error handling into this new try/throws mechanism. That is, when you have an API that presents the following Objective-C method:

- (BOOL)connectToCamera:(NSError **)error;

you end up calling it in Swift 2 as:

try connectToCamera()

This doesn't require any special annotations or other remarks in the source Objective-C code, and works on third-party code as well as Apple's interfaces. The compiler picks up the BOOL response for signifying an error (this also works with NSObject-returning methods that use nil to denote an error) and strips the NSError** parameter. That means that Swift now makes accessing these interfaces safer by default than Objective-C (where it's easy to ignore or miss the error), and makes them easy to incorporate with error-handling code like I use above.

While I was initially skeptical about it, applying the new Swift error handling to our real-world code has provided the same (or better) safety as our previous Result-based code while simplifying it and making it more readable. In a 5500-line project, we were able to cut 130 lines of code by converting to this new model.

Potential areas to improve

All that said, our code drives robotics and electronics in a linear, procedural manner, without employing a lot of asynchronous callbacks. I know that some people have expressed potential concern about these kinds of callbacks with a model like this, so that may deserve a little more research.

The biggest complaint I have about the new Swift error model is the lack of an ability to specify types for errors that can be thrown from a function. If all you're doing is handling NSErrors from Objective-C APIs, this isn't an issue, but we've gone away from using NSError to our own custom error enums. They are easier to work with, easier to verify exhaustive handling, and strongly typed. With a Result type, we could specify an error type that will be passed back from a function. Any code that called a function would know exactly what error(s) they could get back, and we could guarantee exhaustive handling of error cases using properly constructed switch statements.

Even if Swift could make this an optional thing you could use to clarify a function declaration, I'd love to have the ability to write something like this:

func moveToCoordinate(targetCoordinate:Coordinate) throws RoboticsError -> Coordinate

and have a compiler-enforced restriction on the potential errors that could be thrown and received from such a function. I filed a Radar (21362418) asking for this.

Beyond these minor issues, I think the new Swift 2 error system is a pragmatic way of ensuring safety around error handling while getting out of the way of the code you're writing. It handles all of the cases that I used Result<T,U> for. We've converted our Result-based Swift robotics control code over to this, and it is working great in practice.

Syndicate content