Error handling in Swift



In this article we will look at how errors are handled in Swift 2 in such a way that your programs will exit gracefully instead of crashing. If we want to throw an error, the object thrown must conform to the ErrorType protocol. Enums are appropriate for classifying errors, so let’s create one that provides a way of keeping track of all possible errors we could get. Let’s assume this time we can only have two types of errors:

enum Error: ErrorType {
    case InvalidData
    case NoConnection
}

Let’s also create a struct named User that holds a person’s name. If we believe our struct might fail when being instantiated, we can use a failable initializer to ensure that the struct returns nil instead crashing the app.

struct User {
    var firstname = ""
    var lastname = ""
    init? (dict: [String : String]) {
        guard let firstname = dict["firstname"], lastname = dict["lastname"] else {return nil}
        self.firstname = firstname
        self.lastname = lastname
    }
}

Inside the init(:) method we use a guard statement to make sure both the firstname and lastname exist so that the initialization is either successful, or otherwise it fails gracefully.

A function that can throw an error, or calls a function that can throw an error has to be marked with throws. After your program throws an error, you will need to handle that error. Let’s create a function parseData(:) that returns a User struct if the provided argument is valid, or otherwise throws the first of our defined errors, the InvalidData type:

func parseData(from input: [String : String]) throws -> User {
    guard let firstname = input["firstname"], lastname = input["lastname"] else {
        throw Error.InvalidData
    }
    return User(dict: ["firstname" : firstname, "lastname" : lastname])!
}

When calling an error-throwing function, we must embed it in a do block. Within the block, we need to try the function and if it fails, we need to catch it and preferably report an error. When we get to the function that handles the errors, we don’t need to include throws in the declaration. Let’s create another function named testParsing(:) that calls our error-throwing parseData(:) function:

func testParsing(dict: [String : String]) -> String {
    var result = ""
    do {
        let item = try parseData(from: dict)
        result = "\(item.firstname) \(item.lastname)"
    } catch Error.InvalidData {
        result = "Invalid data."
    } catch {
        result = "Unknown error."
    }
    return result
}

If the function doesn’t throw an error, we simply return the user’s name. If it throws an error, however, we need to catch it and report the appropriate error message. You notice we have a second, generic catch statement which is required so that we have an exhaustive handling of the error throwing. Next, let’s see how this function works with various arguments:

print(testParsing(["firstname": "Jane", "lastname": "Doe"]))
print(testParsing(["lastname": "Doe"]))

In the second call, we omitted the firstname in purpose to see how output looks like:

Jane Doe
Invalid data.

As expected, the output for the first call is correct, while the output for the second call reports the error we handled in the testParsing(:) function.

We can also chain multiple throwing function calls together knowing that if any link in the chain fails, the rest of the calls will not execute. Let’s create a method named testConnection(:) that throws the second error we defined in the beginning, the NoConnection type:

func testConnection(response: Int) throws {
    guard response == 200 else {
        throw Error.NoConnection
    }
}

It’s just a dummy function that simulates checking a network connection, and which throws an error when the http return code is different than 200. To test it, let’s also create another function named testThrowing(::) that calls both the throwing functions we created so far:

func testThrowing(dict: [String : String], code: Int) {
    defer {
        print("Testing should be successful, but sometimes errors could be thrown.")
    }
    do {
        try parseData(from: dict)
        try testConnection(code)
        print("All functions completed successfully.")
    } catch {
        print("There is an least one error in the chain!")
    }
}

You will notice that we used the defer keyword which allows the closure that follows it to execute only when the function ends. The advantage of using defer is that the code will execute when the function throws too, so this is a great place to put code that always needs to run, even when errors occur! Next, let’s see how this function works with various arguments:

testThrowing(["firstname": "Jane", "lastname": "Doe"], code: 200)
testThrowing(["firstname": "Jane", "lastname": "Doe"], code: 404)

We first provide correct input for both arguments, and then we provide a bad return code that should lead to a failure, even though the first argument is still correct:

All functions completed successfully.
Testing should be successful, but sometimes errors could be thrown.
There is an least one error in the chain!
Testing should be successful, but sometimes errors could be thrown.

As you expect, the first call returns success while the second reports an error and halts the execution of the entire chain of throwing function calls. This is a powerful feature that we can use whenever we want our program to stop running when there is at least one problem in a long decisional chain. The source code is posted on Github, as usual.

Until next time!