Error handling with Swift 2

On the surface, exception handling in languages such as Java and C# may look very similar to Swift's new error handling pattern; however there are some significant differences. Developers accustomed to exception handling with these languages will notice these differences as they read this section. Before we explore how this error handling works, we need to see how we represent errors in this new error-handling pattern.

In Swift, errors are represented by values of types that conform to the ErrorType protocol. Swift's enumerations are very well suited for modeling these error conditions because, generally, we have a finite number of error conditions to represent. We can also use associated values to add additional information about our errors.

Let's look at how we could use an enumeration to represent an error. For this, we will define a fictitious error named MyError with three error conditions: Minor, Bad, and Terrible:

In this example, we define that the MyError enumeration conforms to the ErrorType protocol. We then define the three error conditions: Minor, Bad, and Terrible. We can also use the associated values with our error conditions to give additional information about the error that occurred. Let's say that we wanted to add a description to one of the error conditions; we would do it like this:

Those who are familiar with exception handling in Java and C# can see that representing errors in Swift is a lot cleaner and easier. Another advantage that we have is that it is very easy to define multiple error conditions and group them together; so all the related error conditions are of one type.

Let's see how we would represent the error conditions for our Drink type:

We begin by defining that the DrinkErrors enumeration conforms to the ErrorType protocol. Within the DrinkErrors enumeration, we define our three error conditions that could occur within our Drink structure. As we mentioned earlier in this section, we can use associated values with our ErrorType enumerations. With associated values, we could rewrite our DrinkErrors enumeration like this:

This is a perfectly acceptable way to define errors in Swift; however, it is not recommended to define them in this way. When an error is thrown in an application, we want to make sure we can catch the specific error so we know how to react to it. In this example we would be able to catch the tempOutOfRange error as it was thrown but we would then need to do an additional lookup to find out if the temperature was too high or too low. Having to do this additional lookup would not be ideal and would add additional complexity that is not necessary.

If we are going to use associated values with our error types, the associated values should be used to add additional information about the error condition rather than being used to specify the type of error that occurred. For example we could use associated values to return the actual temperature as shown in the following example:

Now that we have seen how to define errors, let's look at how we would throw an error if an error condition occurred.

When an error occurs in a function, the code that called the function must be made aware of it; this is called throwing the error. When a function throws an error, it assumes that the code that called it, or some code further up the chain, will catch and recover appropriately from the error.

To throw an error from a function or method we use the throws keyword. This keyword lets the code that called it know that an error may be thrown. Unlike exception handling in other languages, we do not list the specific errors types that may be thrown.

Let's look at how we would throw errors in Swift. We will start by seeing how we would throw an error for our drinking() method. This method will throw an error if we do not have enough volume left in our drink. The error that is thrown will be the insufficentVolume error:

Notice how we start the drinking() method with a comment. This comment block starts off by describing what the method does. We then describe what the amount parameter is and finally we list the errors that could be thrown by this method. When we use comments as shown in this example we are able to hold down the option key and then click on the function name anywhere in our code. Xcode will then display a popup with the description of the function, as shown in the following screenshot:

Throwing errors

Using comments in the way we have just demonstrated will help us document what our method does, what type of parameters they accept, the return value, and what type of errors are thrown from the method. Remember earlier we mentioned that we do not specify the types of errors thrown from our functions, methods, or initializers but, if we document our code as shown here, we can document the error types that could be thrown and what the errors mean.

When we define the drinking() method we define it with the throws keyword at the end of the declaration. The throws keyword indicates that a function, method, or initializer can throw an error. If our function or method had a return type, the throws keyword will appear after the parameter list but before the return type. The following example shows how we would use the throws keyword if we had a return value:

Within the drinking() method, we use the guard statement to verify that we have enough volume left to take our drink. If we do not have enough volume left we throw the DrinkErrors.insufficentVolume error. To throw an error, we use the throw keyword followed by the error we wish to throw. When an error is thrown, control is returned to the code that called the function or method.

Now let's look at how we would write the temperatureChange() method that could throw either the DrinkErrors.tooHot or DrinkErrors.tooCold error:

In the temperatureChange() method, we use comments similar to how we use them in the drinking() method to document what the method does and also what errors could be thrown. Within the method itself, we begin by adjusting the temperature of the drink. We then use two guard statements that verify whether the temperature of the drink is still within the acceptable range. If the temperature is outside the acceptable range, we throw either the DrinkErrors.tooHot or DrinkErrors.tooCold error.

Now let's look at how we would catch the errors that are thrown.

When an error is thrown from a function, we need to catch it in our code; this is done using the do-catch block. The do-catch block takes the following syntax:

When we call a function or method that throws an error, we must prefix the call with the try keyword. The try keyword helps us quickly identify the code that could potentially throw errors.

If an error is thrown, it is propagated out until it is handled by a catch clause. The catch clause consists of the catch keyword, followed by a pattern to match the error against. If the error matches the pattern, the code within the catch block is executed.

Let's see how we would catch the errors thrown from the drinking() method:

In this example, we surrounded the call to the drinking() method with the do-catch block and we also prefixed the call to the drinking() method with the try keyword. The catch statement attempts to match the DrinkErrors.insufficentVolume error since that is the error thrown by the drinking() method.

If the DrinkErrors.insufficentVolume error is thrown from the drinking() method we then print the message Error taking drink to the console.

We do not have to include a pattern after the catch statement. If a pattern is not included after the catch statement or we put an underscore, the catch statement will match all error conditions. For example, either one of the following two catch statements will catch all errors:

If we are unsure of the errors that may be thrown from a function or method it is a good idea to include a catch statement that will match all error conditions to avoid having an error at runtime that is not caught. If we have an error at runtime that is not caught the application will crash.

If we want to capture the error, we can use the let keyword, as shown in the following example:

Now let's see how we would catch the DrinkErrors.tooHot and DrinkErrors.tooCold errors from the temperatureChange() method:

In this example, we have two catch statements where each catch statement matches a different error condition. In addition we also capture the associated value for the error by using the let statement within the parentheses, as shown in this example.

When I am working with exceptions in languages such as Java and C#, I see a lot of empty catch blocks. This is where we need to catch the exception because one might be thrown; however, we do not want to do anything with it. In Swift, the code would look something like this:

Seeing code like this is one of the things that I dislike about exception handling. The Swift developers have an answer for this: the try? keyword. The try? keyword attempts to perform an operation that may throw an error. If the operation succeeds, the results, if any, are returned in the form of an optional; however, if the operation fails with an error being thrown, the operation returns a nil and the error is discarded. We would use the try? keyword like this:

If the function or method had a return type, we would use optional binding to capture the value returned, as shown in the next example:

As we can see, the try? keyword makes our code much cleaner and easier to read.

We can also let the errors propagate out rather than immediately catching them. To do this, we just need to add the throws keyword to the function definition. For instance, in the following example rather than catching the error, we let it propagate out to the code that called the function rather than handling the error within the function:

If we need to perform some clean up action, regardless of whether we had any errors or not, we can use the defer statement. We use the defer statement to execute a block of code just before code execution leaves the current scope. The following example shows how we would use the defer statement:

If we called this function, the first line that is printed to the console is: Function started. The execution of the code skips the defer block and Function finished would be printed to the console next. Finally, the defer code block will be executed just before we leave the function's scope, and we see the message: In defer block. The following is the output from this function:

The defer block will always be called before execution leaves the current scope, even if an error is thrown. The defer block is very useful when we need to perform some clean up functions prior to leaving a function.

The defer statement is very useful when we want to make sure we perform all the necessary clean up, even if an error is thrown. For example, if we successfully open up a file to write to, we will always want to make sure we close that file, even if we have an error during the write operation. We could then put the file-closed functionality in a defer block to make sure that the file is always closed prior to leaving the current scope.

In this chapter we have seen three ways that we can do error handling in Swift, but which one should we use?