Error Handling in Swift

Episode #142 | 12 minutes | published on October 23, 2014 | Uses swift-1.0
Subscribers Only
In this episode I talk about the pattern of communicating errors in Cocoa and how it can be improved by leveraging features in Swift. By introducing a Result type that is generic and applies to any type, it appears useful, however we run into some cumbersome use cases that will require further discussion.

Episode Links

Cocoa Pattern for returning errors

In Cocoa, we're used to the following pattern when we have a method that has a return type, but can fail and optionally give us back an NSError pointer indicating what went wrong.

This is used for writing files, reading JSON, fetching from a database, and loads of other cases.

NSError *error = nil;
id json = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingOptionsAllowFragments error:&error];
if (json == nil) {
  NSLog("We got an error: %@", [error localizedDescription]);
} else {
  NSLog("JSON was parsed successfully.");
}

It's all to easy to just pass nil for the error parameter and ignore it. In addition, some APIs inform you not to rely on the error pointer being present, but instead check the return value. These add up to the fact that it's too easy to just ignore errors reported by methods.

In Swift, we can take advantage of its features to implement something a little more formalized.

import Foundation

var jsonString = "{ \"foo\": \"bar\" }"
var data = jsonString.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true)!

var error: NSError?
var json: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: .AllowFragments, error: &error)

if json != nil {
    json
} else {
    error
}

// This box class only exists to pacify the compiler
// which as of Xcode 6.0.1 it crashes SourceKit if
// you have a generic param used in a case value.
// This will likely be unnecessary in future versions 
// of the compiler.
class Box<T> {
    var unbox: T
    init(_ value: T) {
        unbox = value
    }
}

enum Result<T> {
    case Success(Box<T>)
    case Error(String)
}

func divide(x: Int, y: Int) -> Result<Float> {
    if y == 0 {
        return .Error("divide by zero")
    } else {
        return .Success(Box(Float(x) / Float(y)));
    }
}

// Should we have compute take a result?  It feels awkward and splits
// handling of the result across multiple methods.  We'll address this later.
func compute(input: Result<Float>) -> Result<Float> {
    switch input {
    case .Success(let inputValue):
        if inputValue.unbox < 0 {
            return .Error("can't work with negative numbers")
        }
        return .Success(Box(log(inputValue.unbox)))

    case .Error: return input
    }
}

switch compute(divide(12, -4)) {
case .Success(let value): value.unbox
case .Error(let error): error           // => Error: "can't work with negative numbers"
}
blog comments powered by Disqus