fbpx

Blogs from the Ranch

< Back to Our Blog

Swift 2.0 Error Handling

Avatar

Matt Mathias

As my fellow Nerd Juan Pablo Claude pointed out in his post on Error Handling in Swift 2.0, the latest iteration of Swift introduces many features, and a new native error handling model is notable among these.

Prior to its 2.0 release, Swift did not provide a native mechanism for error handling. Errors were represented and propagated via the Foundation class NSError. In focusing on enumerations, the new model for error handling in Swift 2.0 feels more idiomatic.

While Juan Pablo’s post offered a bit of history and differences between Objective-C and Swift error handling, today I’d like to dive into the differences in error handling between Swift 1.2 and Swift 2.0.

Error Handling in Swift 1.x

In Swift 1.x, a reference to an NSError was passed to an NSErrorPointer parameter in a function or method to fill in error information if it exists.
And while NSError was serviceable, its strong emphasis on string error domains and integer error codes made it difficult to examine an error and know exactly what to do with it.

If you wrote any Swift 1.x code, then you might have written something like:

var theError: NSError?

func doSomethingThatMayCauseError(error: NSErrorPointer) -> Bool {
    // do stuff...
    var success = true
    // if there is an error, then make the error, and set the return to `false`
    if error != nil {
        error.memory = NSError(domain: "SuperSpecialDomain", code: -99, userInfo: [
            NSLocalizedDescriptionKey: "Everything is broken."
            ])
        success = false
    }
    return success
}

let success = doSomethingThatMayCauseError(&theError)

if !success {
    if let error = theError {
        println(error.localizedDescription) // "Everything is broken."
    }
}

As the example demonstrates, error handling in Swift 1.x was constrained by an intellectual debt to NSError. This constraint meant that code had to create an optional instance of NSError, and pass its pointer to some function that took an NSErrorPointer. That pointer was then later used in the function to fill it out with error information if there was a problem. If a non-nil pointer was indeed passed into the function’s error parameter, then an appropriate error was created and assigned to the NSErrorPointer’s memory property.

Since the function returned true on success, that was the first thing to check. If the return was false, then theError needed to be unwrapped because there was a problem. Examining the error would then give a little more information on what went wrong.

This process adds a lot of mental overhead. It adds boilerplate code (e.g., the optional binding syntax) and we even have to use a strange new type called NSErrorPointer. It is reliant upon an Objective-C class (NSError) and requires that we wrap a pointer to this class in a new type made just for Swift. NSError also forces us to use optionals, but we would obviously much prefer a more straightforward mechanism: Is there an error or not? It would be better to know decisively that there is an error, but the above code instead relies upon our interrogation of an NSError instance.

Last, NSError does not make it easy to represent and check error information exhaustively. There are many occasions where you may get an error back, but depending upon the domain and the code, you may not actually care about that particular error. Because NSError buries that information in its properties, you have to write code to make sure that the error you received is the one you are prepared to handle.

Error Handling in Swift 2.0

Swift’s new model for error handling is much more idiomatic. A new protocol called ErrorType represents a native mechanism for representing errors. ErrorType is designed to work specifically with enumerations. You create an enumeration to model the sorts of errors you expect to encounter. This means that you create error types that comprise a known set of some sort of error (more on this soon).

Error handling in Swift 2.0 takes advantage of Swift’s powerful enumerations. The associated values on an enum’s cases make it easy to pack additional error information into an instance of the enum. Furthermore, listing potential errors in the cases of an enum makes error handling more complete, predictable and exhaustive: You handle the errors you expect and can do something with.

An Example Database

Consider an example that seeks to build a naive database to house users and their movie ratings.

First, we have a Movie type.

struct Movie {
    let name: String
}

Movies are simple; they just have a name.

Since Movie is a value type, let’s be sure to conform to Equatable; we will take advantage of this functionality later on.

extension Movie: Equatable {}

func ==(lhs: Movie, rhs: Movie) -> Bool {
    return lhs.name == rhs.name
}

Movies can be rated with a Rating enum.

enum Rating {
    case Bad, Okay, Good
}

Movies can be .Bad, .Okay and .Good.

Movies are rated by a User. Users just have names and emails. Again, because User is a value type, we will be sure to make User conform to Equatable.

struct User {
    let name: String
    let email: String
}

extension User: Equatable {}

func ==(lhs: User, rhs: User) -> Bool {
    return lhs.name == rhs.name && lhs.email == rhs.email
}

User’s conformance to Equatable simply checks to see if the two instances have the same name and email.

A MovieRating type ties all of these together and will represent a table for movie ratings in our database.

struct MovieRating {
    let rating: Rating
    let rater: User
    let movie: Movie
}

Instances of MovieRating have properties for ratings, raters and movies.

As above, we have MovieRating conform to Equatable.

extension MovieRating: Equatable {}

func == (lhs: MovieRating, rhs: MovieRating) -> Bool {
    return lhs.rating == rhs.rating && lhs.rater == rhs.rater && lhs.movie == rhs.movie
}

We are almost ready to make our movie review database. Before we begin, it is worthwhile to consider our database’s API. Since Swift 2.0’s error handling model relies upon enumerations, it is easy to think about the sort of errors we may encounter while we are designing a type’s implmentation.

Let’s start out with this implementation of our database’s error type.

enum DatabaseError: ErrorType {
    case InvalidUser(User)
    case MoviePreviouslyRated(Movie)
    case DuplicateEmailAddress(String)
}

The enum DatabaseError sketches out the sort of functionality that we can expect the database to perform by listing the possible errors we may expect from it. This helps to set our expectations for the database’s API. Just glancing at this enum tells us that queries to the database may involve an invalid user, something having to do with previously rated movies, and a duplicate email address.

Notice that the database’s error type is modeled as an enumeration.
This helps the error type to comprehensively list out all of the errors that we can expect from it. The enumeration tells us that it is modeling errors via its conformance to the ErrorType protocol. Since DatabaseError is a regular Swift enumeration, we can even add associated values to particular cases on the enum to give useful error information to an instance of DatabaseError.

Let’s implement our Database to see how this enum will be used.

class Database {
    private(set) var users: [User] = []
    private(set) var movies: [Movie] = []
    private(set) var ratings: [MovieRating] = []

    func createUser(withName name: String, email: String) throws -> User {
        let userEmails = users.map { $0.email }

        if userEmails.contains(email) {
            throw DatabaseError.DuplicateEmailAddress(email)
        }

        let newUser = User(name: name, email: email)
        users.append(newUser)
        return newUser
    }
}

The Database is a class with three stored properties.
One for the users in the database, another for the movies that users have rated, and a final property for movie ratings. For simplicity, these are stored properties with default values of empty arrays.

Creating Users

Database currently has only a single method—one for creating users and inserting them into the database. The method createUser has parameters for all of the inputs needed to create a user. After the parameter list, however, there is a new keyword being used: throws.

throws marks the method as potentially generating an error.
For example, it is possible that somebody will try to create a user with the exact same email address as another user (which this database uses to differentiate users). If this should happen, then the function throws an error. The error will tell the caller what went wrong.
In this case, the error will be .DuplicateEmailAddress and will carry with it the duplicated email address in the case’s associated value.

If no error is encountered, then the method will append the newUser to Database’s users array. Last, createUser will return the new user.

You might think that the createUser function has the following type: (String, Int, [Movie]) -> User. After all, that’s what the type would be in Swift 1.2. But this is Swift 2.0, and there is this weird throws keyword. As you now know, throws means that this method may generate an error. Indeed, throws tells the compiler that this function is different. It is so different that it has a different type: (String, Int, [Movie]) throws -> User.

Let’s create an instance of the Database to make a new user and see how error handling in Swift 2.0 works.

var db = Database()

do {
    var alana = try db.createUser(withName: "Alana", email: "alana@example.com")
} catch DatabaseError.DuplicateEmailAddress(let email) {
    print("(email) already exists in the database.")
} catch let unknownError {
    print("(unknownError) is an unknown error.")
}

Since createUser can throw an error, we have to use a do-catch statement to capture and respond to errors.

Inside of the do block, we try to call a function that may throw an error. In this case, a call to createUser may succeed, but it also may fail. The reason that it may fail is if that new user is created with a duplicate email address.

This error is captured in the catch block, which looks and works like a switch statement. Here, we check to see if the error matches a specific case on the DatabaseError enum: .DuplicateEmailAddress. Since this case has an associated value, we can bind this value to a constant (email in this example), and use it to respond to the error. In the example above, all we do is log that invalid email to the console. A more “real-world” application might do something more meaningful like let the user of the application know that a particular email has already been taken.

Notice that the do-catch statement above is concluded with a “catch-all” block. This catch does not provide any pattern to match an error against, and so it will capture any error that comes through. It does specify a new local constant to bind this uncaught error to: unknownError. If this constant were not specified, then the compiler would have bound the error to a constant named error to be used locally within this catch. Similar to switch statements that must exhaustively address each potential case, Swift’s do-catch statements must exhaustively capture all possible errors.
The “catch-all” above works like a default case on a switch.

You might think that you can omit this “catch-all” by simply implementing a catch for each possible error. Unfortunately, Swift’s compiler cannot know that you are covering all errors in this manner. All the compiler knows is that you called a function that can throw an instance of some type that conforms to ErrorType. Thus, it cannot know all of the possible errors that could be encountered. This limitation is overcome by using a catch without an error pattern to match, which is called a “catch-all.”

(Incidentally, if you are following along in playground, the compiler will not require that you have this “catch-all.” However, a regular Xcode project will give you a compiler error telling you that you have not caught all possible errors if you omit the “catch-all”.)

Adding a Movie Rating

Now that alana is in the database, let’s give this user a movie rating.
Let’s first make an add method on the database.

func add(movie movie: Movie, withRating rating: Rating, forUser user: User) throws {
    if !users.contains(user) {
        throw DatabaseError.InvalidUser(user)
    }

    let userRatedMovies = ratings.filter { $0.rater == user }.map { $0.movie }
    if userRatedMovies.contains(movie) {
        throw DatabaseError.MoviePreviouslyRated(movie)
    }

    if !movies.contains(movie) {
        movies.append(movie)
    }
    ratings.append(MovieRating(rating: rating, rater: user, movie: movie))
}

The method add takes a movie, a rating and a user as arguments. It throws an error should the user not exist in the database, or if the user previously rated the movie. Hence, this method can throw two potential errors.

If the user is valid and the movie has not been previously rated by that user, then the movie is added to the database’s ratings property. If the movie has not already been added to the database, we also add it to the array of movies in the database.

Now we can use the add method to give a movie rating to the database.

do {
    var alana = try db.createUser(withName: "Alana", email: "alana@emaple.com")
    let darko = Movie(name: "Donnie Darko")
    try db.add(movie: darko, withRating: .Good, forUser: alana)
} catch DatabaseError.DuplicateEmailAddress(let email) {
    print("(email) already exists in the database.")
} catch DatabaseError.InvalidUser(let user) {
    print("(user) is not in the database.")
} catch DatabaseError.MoviePreviouslyRated(let movie) {
    print("User has already rated (movie.name)")
} catch let unknownError {
    print("(unknownError) is an unknown error.")
}

We create an instance of Movie, then attempt to add that movie to the database. Notice that we have added two new catches. These help to capture errors related to alana not being in the database and the movie Donnie Darko already being rated by alana.

The do-catch statement now does several things. It creates an instance of User, creates a Movie instance, and adds that instance to the database. The code above also catches all of the possible errors that may arise from these operations. Of course, a real app should do something more useful with these error than simply logging them to the console.

try, but do not try!

Any method that is marked with throws needs to be dealt with appropriately. Consider the following example:

func myFunc() throws { ... }

If you were to call myFunc (e.g., myFunc()), then the compiler will yell at you: Call can throw but is not marked with 'try'. Functions (and methods) that throw errors must be tried when called: try myFunc(). It is up to you to handle the errors correctly, but the compiler will require that you at least try.

In keeping with Swift’s emphasis on compile time safety, we can now mark functions as potentially throwing an error. This changes the type of the function, which differentiates a function that throws from a function that does not. Calling a function that throws means that you are required to try the function when you call it.

Swift 2.0 provides us with a mechanism to short circuit this safety if needed. In the code above, myFunc throws some sort of error. Calling myFunc without a try will lead to a compiler error. However, you can circumvent this requirement by using try!: try! myFunc(). This will prevent the error from being forwarded on to you. It essentially tells the compiler that you do not care about the potential errors that may arise from calling the function.

As you might expect, using try! is dangerous. The exclamation point (!) should stir within you the same fear and responsibility it does when forcibly unwrapping optionals. If there is an error, function call marked with try! will cause a runtime error, which will crash your application. You should only use try! when you are absolutely sure the call will not generate an error, or if you are absolutely sure you want to crash if there is an error. In either case, try! should be used sparingly and should require strong justification.

Final Thoughts

Swift 2.0’s model for error handling brings a native mechanism to handling errors. In brief, its use of enumerations allows for a do-catch statement that works similarly to a switch statement, and enumerations provide an elegant way to capture the errors that may be thrown. Functions marked with throws must be called with try. These functions should signal to you that you need to handle some error.

Avatar

Matt Mathias

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project