Error Handling in Swift 2

I have yet to meet a programmer who enjoys error handling. Whether you like it or not, a robust application needs to handle errors in such a way that the application remains functional and informs the user when necessary. Like testing, it's part of the job.

1. Objective-C

In Objective-C, it was all too easy to ignore error handling. Take a look at the following example in which I ignore any errors that may result from executing a fetch request.

The above example shows that error handling in Objective-C is something the developer needs to opt into. If you'd like to know what went wrong if something goes haywire, then you tell this to the API by handing it an NSError pointer. The example below illustrates how this works in Objective-C.

While earlier versions of Swift didn't come with a good solution for error handling, Swift 2 has given us what we've asked for and it was well worth the wait. In Swift 2, error handling is enabled by default. In contrast to Objective-C, developers need to explicitly tell the compiler if they choose to ignore error handling. While this won't force developers to embrace error handling, it makes the decision explicit.

If we were to translate the above example to Swift, we would end up with the same number of lines. While the amount of code you need to write remains unchanged, the syntax makes it very explicit what you are trying to do.

At the end of this tutorial, you will understand the above code snippet and know everything you need to know to handle errors in Swift 2.

2. Throwing Functions

throws

The foundation of error handling in Swift is the ability for functions and methods to throw errors. In Swift parlance, a function that can throw errors is referred to as a throwing function. The function definition of a throwing function is very clear this ability as illustrated in the following example.

The throws keyword indicates that init(contentsOfURL:options:) can throw an error if something goes wrong. If you invoke a throwing function, the compiler will throw an error, speaking of irony. Why is that?

try

The creators of Swift have put a lot of attention into making the language expressive and error handling is exactly that, expressive. If you try to invoke a function that can throw an error, the function call needs to be preceded by the try keyword. The try keyword isn't magical. All it does, is make the developer aware of the throwing ability of the function.

Error Handling Without try

Wait a second. The compiler continues to complain even though we've preceded the function call with the try keyword. What are we missing?

Error Handling With try

The compiler sees that we're using the try keyword, but it correctly points out that we have no way in place to catch any errors that may be thrown. To catch errors, we use Swift's brand new do-catch statement.

do-catch

If a throwing function throws an error, the error will automatically propagate out of the current scope until it is caught. This is similar to exceptions in Objective-C and other languages. The idea is that an error must be caught and handled at some point. More specifically, an error propagates until it is caught by a catch clause of a do-catch statement.

In the updated example below, we invoke the init(contentsOfURL:options:) methods in a do-catch statement. In the do clause, we invoke the function, using the try keyword. In the catch clause, we handle any errors that were thrown while executing the function. This is a pattern that's very common in Swift 2.

In the catch clause, you have access to the error that was thrown through a local constant error. The catch clause is much more powerful than what is shown in the above example. We'll take a look at a more interesting example a bit later.

3. Throwing Errors

In Objective-C, you typically use NSError, defined in the Foundation framework, for error handling. Because the language doesn't define how error handling should be implemented, you are free to define your own class or structure for creating errors.

This isn't true in Swift. While any class or structure can act as an error, they need to conform to the ErrorType protocol. The protocol, however, is pretty easy to implement since it doesn't declare any methods or properties.

Enumerations are powerful in Swift and they are a good fit for error handling. Enums are great for the pattern matching functionality of the catch clause of the do-catch statement. It's easier to illustrate this with an example. Let's start by defining an enum that conforms to the ErrorType protocol.

We define an enum, PrinterError, that conforms to the ErrorType protocol. The enum has four member variables. We can now define a function for printing a document. We pass the function an NSData instance and tell the compiler that it can throw errors by using the throws keyword.

To print a document, we invoke printDocumentWithData(_:). As we saw earlier, we need to use the try keyword and wrap the function call in a do-catch statement. In the example below, we handle any errors in the catch clause.

We can improve the example by inspecting the error that is thrown. A catch clause is similar to a switch statement in that it allows for pattern matching. Take a look at the updated example below.

That looks much better. But there is one problem. The compiler is notifying us that we are not handling every possible error the printDocumentWithData(_:) method might throw.

Error Handling Needs to Be Exhaustive

The compiler is right of course. A catch clause is similar to a switch statement in that it needs to be exhaustive, it needs to handle every possible case. We can add another catch clause for PrinterError.MaintenanceRequired or we can add a catch-all clause at the end. By adding a default catch clause, the compiler error should disappear.

4. Cleaning Up After Yourself

The more I learn about the Swift language, the more I come to appreciate it. The defer statement is another wonderful addition to the language. The name sums it up pretty nicely, but let me show you an example to explain the concept.

The example is a bit contrived, but it illustrates the use of defer. The block of the defer statement is executed before execution exits the scope in which the defer statement appears. You may want to read that sentence again.

It means that the powerOffPrinter() function is invoked even if the printData(_:) function throws an error. I'm sure you can see that it works really well with Swift's error handling.

The position of the defer statement within the if statement is not important. The following updated example is identical as far as the compiler is concerned.

You can have multiple defer statements as long as you remember that they are executed in reverse order in which they appear.

5. Propagation

It is possible that you don't want to handle an error, but instead let it bubble up to an object that is capable of or responsible for handling the error. That is fine. Not every try expression needs to be wrapped in a do-catch statement. There is one condition though, the function that calls the throwing function needs to be a throwing function itself. Take a look at the next two examples.

The first example results in a compiler error, because we don't handle the errors that printDocumentWithData(_:) may throw. We resolve this issue in the second example by marking the printTestDocument() function as throwing. If printDocumentWithData(_:) throws an error, then the error is passed to the caller of the printTestDocument() function.

6. Bypassing Error Handling

At the beginning of this article, I wrote that Swift wants you to embrace error handling by making it easy and intuitive. There may be times that you don't want or need to handle the errors that are thrown. You decide to stop the propagation of errors. That is possible by using a variant of the try keyword, try!.

In Swift, an exclamation mark always serves as a warning. An exclamation mark basically tells the developer that Swift is no longer responsible if something goes wrong. And that is what the try! keyword tells you. If you precede a throwing function call with the try! keyword, also known as a forced-try expression, error propagation is disabled.

While this may sound fantastic to some of you, I must warn you that this isn't what you think it is. If a throwing function throws an error and you've disabled error propagation, then you'll run into a runtime error. This mostly means that your application will crash. You have been warned.

7. Objective-C APIs

The Swift team at Apple has put a lot of effort into making error handling as transparent as possible for Objective-C APIs. For example, have you noticed that the first Swift example of this tutorial is an Objective-C API. Despite the API being written in Objective-C, the method doesn't accept an NSError pointer as its last argument. To the compiler, it's a regular throwing method. This is what the method definition looks like in Objective-C.

And this is what the method definition looks like in Swift.

The errors that executeFetchRequest(request: NSFetchRequest) throws are NSError instances. This is only possible, because NSError conforms to the ErrorType protocol as we discussed earlier. Take a look at the Conforms To column below.

NSError Class Reference

Learn More in Our Swift 2 Programming Course

Swift 2 has a lot of new features and possibilities. Take our course on Swift 2 development to get you up to speed. Error handling is just a small piece of the possibilities of Swift 2.

Conclusion

The takeaway message of this article is that error handling rocks in Swift. If you've paid attention, then you've also picked up that you will need to adopt error handling if you choose to develop in Swift. Using the try! keyword won't get you out of error handling. It's the opposite, using it too often will get you into trouble. Give it a try and I'm sure you're going to love it once you've given it some time.

Tags:

Comments

Related Articles