Back to Home



Understanding Error Handling in Swift 2

 

 Craig A. Will

 June, 2015

In Swift 2 a sophisticated and well-designed system for handling errors has been introduced.

 

Swift was criticized early on for not having a try-catch error capability like many other languages. (The criticism is somewhat ironic because try-catch is not widely used in Objective-C, with the simpler NSError system typically used instead, when it is used at all.)

 

Even so, the Swift team has responded by producing an error handling system that is similar to the common try-catch schemes in other languages, but with significant differences. For example, code that can throw an error (exception) must be placed in a method that has the keyword throws as part of its declared syntax (and type signature). And such methods are called with the keyword try. Both of these help to clearly indicate what part of the code is throwing the error.

 

Handling Errors in Swift 1

 

In Swift 1, errors have been handled in a number of ways. One approach was to use the NSError object that is commonly used in Objective-C for error handling: When an API, in particular, was called, an NSError object was created (often called err), set to nil, and passed to the API. If the task was successful, the NSError object would remain nil. If there was an error, a description of it would be placed in the NSError object. The first step after the API call was accomplished was to test the NSError object for nil, and, if it was not, deal with the particular error.

 

Some developers used the enumeration capability in Swift to handle errors in a different way: An enumeration, typically called Result, was defined as follows:

 

    Enum Result <T,  E> {

        Case Success(T)

        Case Failure(E)

    }

 

A task was then done with one of the enumeration values returned when it had completed. In the case of success, the type T contains some result of the success. In the case of failure, the type E contains a description of the particular error encountered. (E might be an NSError object).

 

In some cases, developers using these techniques initially reacted negatively to the new Swift 2 approach, indicating that the techniques they were using were fine. However, after some experimentation coding the new approach, these developers decided that the new approach resulted in safer, more readable code.

 

Swift 2’s Approach

 

We can see how Swift’s error handling works with a simple example.  For example, an app might attempt to access a file on a remote server, and encounter one of the following errors:

 

1. The app cannot access the Internet.

2. The file on the remote server cannot be found.

3. The file has been successfully downloaded, but there is a format problem.

 

I’ll keep this simple with just these three errors, but obviously a lot more can go wrong in this situation.

 

These are recoverable errors (they don’t crash the app) that most commonly will result in a message to the user of the app indicating the error so that user can respond. In other cases the recovery might involve just code. And in the case of an unrecoverable error the error handling code might terminate the app, hopefully with an informative message to the user.

 

Defining Errors as Enumeration Member Values

 

The first step toward implementing Swift’s error handling is to define an enumeration that includes member values for each of the possible errors:

 

    enum FileError: ErrorType{

        case cannotAccessInternet

        case cannotAccessFile

        case fileFormatError

    }

 

Note the ErrorType in the first line of the definition of the enumeration. This indicates that the enumeration conforms to the ErrorType protocol.

 

Executing Error-Prone Code in a Function/Method

 

In Swift, the code that is to be executed that might result in an error is placed into a function or method. The function or method is then called as part of a do-try-catch sequence.

 

The function or method that is executing the might-fail code has the keyword throws included in its definition, just after the input parameters. Thus:

 

    func getAFile(filename: String) throws -> String {

            //  Code to go to a remote server and

            //  get the contents of a file goes here

    }

 

The keyword throws indicates that the function or method has the ability to throw an error.  “Throw an error” means that the function or method, when an error is found, executes a statement consisting of the keyword throw together with an enumeration value indicating the specific error. This causes the flow of control to immediately leave the function or method and to be processed by one or more catch statements.

 

Thus, for example, if the code in a function getAFile determines that it cannot access the Internet, it might execute the following statement:

 

    throw FileError.cannotAccessInternet

 

If the throws keyword is not included in the definition of a function or method, that function or method cannot throw an error.

 

The method to be executed can use if-else statements to throw errors:

 

     func getAFile(filename: String) throws -> String {

                // Code to be executed that might produce an error goes here

                //  before attempting to download let’s make sure we have an

                //  an Internet connection; the app has a Boolean variable that maintains

                //  this information

        if !gotInternetConnection [

            throw FileError.cannotAccessInternet

        }

        else {

                                    // Here we attempt to download a file from a server

               // by providing an iOS API with a URL

                                    // the file ends up as a string with fileContents

                                    // but if the file could not be found fileContents is nil

                                    // Likely an HTTP 404 but has been transformed.

            if fileContents == nil {

                throw FileError.cannotAccessFile

            }

            else {

                // here we (after unwrapping it, it is an optional)

                                      // try to do something with the file, it is likely

                                      // JSON and we want to convert it to something easier

                                      // to deal with, but that conversion can fail

                if formatError {

                    throw FileError.fileFormatError

                }

                // Success!

                else {

                    print(“File downloaded OK and is in correct format”)

                    // Do more stuff with the file

                }

        }

    }

Using Guard Statements

 

It is often convenient to use guard statements rather than if-else statements. The guard statement works like if, but has a different syntax and flow. It is basically an if statement with the logic inverted. If the “if” condition is true, it does not execute an associated statement that is contained within curly braces. Instead, control passes to the statement just after the end of the “else” part of the guard statement. Note that there is no pair of curly braces analogous to those for the “if” statement. This makes sense, given where control is passed. It also means that any variables or constants created after the guard keyword will remain in scope after the end of the guard statement.

 

The guard statement requires that the else statement transfer control out of the method. (A throw will do that.) The else part of the statement is thus, obviously, required.

 

Eric Cerney,  in a discussion (ericcerney.com/swift-guard-statement) about why he likes Swift guard statements, calls the pattern that guard statements make easy to create the “Bouncer  Pattern”. The idea is to quickly get rid of the bad cases (like a bouncer for a bar does), then you can focus on what needs to be done in the good case.

 

The following code does the same thing as the code above, but uses guard statements.

 

    func getAFile(filename: String) throws -> String {

        // Code to be executed that might produce an error goes here

                   // before attempting to download let us make sure we have an

                   // an internet connection; the app has a Boolean variable that maintains

                   // this information

        guard gotInternetConnection else {

            throw FileError.cannotAccessInternet

        }

        // here we attempt to download a file from a server

                   // by providing an iOS API with a URL

                   // the file ends up as a string with fileContents

                   // but if the file could not be found fileContents is nil

                   // Likely an HTTP 404 but has been transformed.

        guard !fileContents != nil

            else {

                throw FileError.cannotAccessFile

            }

        // here we (after unwrapping it, it is an optional)

                   // try to do something with the file, it is likely

                   // JSON and we want to convert it to something easier

                   // to deal with, but that conversion can fail

        guard !formatError  else {

                    throw FileError.fileFormatError

                }

        // Success!    

        print(“File downloaded OK and is in correct format”)

                    // do more stuff with the file

}

 

This is much cleaner than the previous code that used a sequence of nested if-else statements.

 

The Do-Try-Catch Sequence

 

When the function or method that contains the code in question is to be executed, it is done so using the keyword try in front of the function call:

 

    try getAFile("filename")

 

This call to the function or method is part of a do-try-catch sequence, as follows:

 

    do {

        try getAFile("filename")

        print("File retrieved with no errors")

    } catch FileError.cannotAccessInternet

      {

        print(“Cannot access Internet—Make sure you have an Internet connection.”)

      }

      catch FileError.cannotAccessFile

      {

        print(“Cannot access requested file on remote server—Make sure you have provided the correct name for the file.”)

      }

      catch FileAccessError.fileFormatError

      {

        print(“There is something wrong with the specified file—Make sure that this is the correct file.”)

      }

 

Note that there is only one try statement, but multiple catch statements. This is different from languages like Java, which use try-catch statements in pairs, with only one catch for each try.

 

 

If the getAFile call succeeds with no throwing of an error, any code following that call will be executed, such as the print statement shown in the example.

 

However, if an error is thrown, statements after the call will not be executed, and control will pass immediately to the catch statement.

 

It is common for the catch clauses to simply use the same enumeration value that was used in the throw statement. However, the catch clauses can use the same sophisticated matching capabilities that switch statements have if desired.

 

If you are sure that a function or method will not execute code that results in an error,  you can use the try! keyword instead of try. This will disable the throwing of any errors.

 

Defer

 

The defer keyword allows you to define code that will be executed just before flow of execution leaves the current scope. (It doesn’t matter how the current scope was exited—by returning from a function or method, generating an error/exception, or just reaching a right curly brace.)

 

Thus:

 

    defer {  close(“filename”) }

 

is a common use, since you usually want to close a file once you’ve read from it, or attempted to read from it, regardless of what happened.

 

This code (analogous to the finally statement in languages like JavaScript), makes sure that this code is executed, regardless of whether an error has been thrown or not.

 

It is allowed to have multiple defer statements, each with a block of code that you absolutely need to get executed, whatever happens.

 

The defer statement makes it easy to put code close to related code, rather than at the end. It helps avoid errors, since it is no longer necessary to put cleanup code (or calls to it) in multiple branches, depending upon what happens.