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.
// Execute Fetch Request NSArray *results = [managedObjectContext executeFetchRequest:fetchRequest error:nil]; if (results) { // Process Results ... }
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.
NSError *error = nil; // Execute Fetch Request NSArray *results = [managedObjectContext executeFetchRequest:fetchRequest error:&error]; if (error) { // Handle Error ... } else { // Process Results ... }
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.
do { // Execute Fetch Request let results = try managedObjectContext.executeFetchRequest(fetchRequest) // Process Results ... } catch { let fetchError = error as NSError // Handle Error ... }
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.
init(contentsOfURL url: NSURL, options readOptionsMask: NSDataReadingOptions) throws
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?
let data = NSData(contentsOfURL: URL, options: [])
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.
Wait a second. The compiler continues to complain even though we've preceded the function call with the try
keyword. What are we missing?
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.
do { let data = try NSData(contentsOfURL: URL, options: []) } catch { print("\(error)") }
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.
enum PrinterError: ErrorType { case NoToner case NoPaper case NotResponding case MaintenanceRequired }
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.
func printDocumentWithData(data: NSData) throws { ... }
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.
do { try printDocumentWithData(data) } catch { // Handle Errors }
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.
do { try printDocumentWithData(data) } catch PrinterError.NoToner { // Notify User } catch PrinterError.NoPaper { // Notify User } catch PrinterError.NotResponding { // Schedule New Attempt }
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.
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.
do { try printDocumentWithData(data) } catch PrinterError.NoToner { // Notify User } catch PrinterError.NoPaper { // Notify User } catch PrinterError.NotResponding { // Schedule New Attempt } catch { // Handle Any Other Errors }
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.
func printDocumentWithData(data: NSData) throws { if canPrintData(data) { powerOnPrinter() try printData(data) defer { powerOffPrinter() } } }
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.
func printDocumentWithData(data: NSData) throws { if canPrintData(data) { defer { powerOffPrinter() } powerOnPrinter() try printData(data) } }
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.
func printTestDocument() { // Load Document Data let dataForDocument = NSData(contentsOfFile: "pathtodocument") if let data = dataForDocument { try printDocumentWithData(data) } }
func printTestDocument() throws { // Load Document Data let dataForDocument = NSData(contentsOfFile: "pathtodocument") if let data = dataForDocument { try printDocumentWithData(data) } }
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.
- (NSArray *)executeFetchRequest:(NSFetchRequest *)request error:(NSError **)error;
And this is what the method definition looks like in Swift.
public func executeFetchRequest(request: NSFetchRequest) throws -> [AnyObject]
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.
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.
Comments