Controlling chaos: Error Handling in Swift 4 with do, try, catch, defer, throw, throws, Error, and NSError

[Download the Xcode 9 playground from GitHub so you can follow along with my detailed discussion and try iOS error handling using the do-try-catch feature yourself!]

This tutorial is the second in my series of tutorials on incorporating error checking into your Swift 4-based iOS apps. Remember that by “error checking,” I mean gracefully handling unexpected values, events, and conditions that arise during program execution.” Today, I’ll limit my discussion of error checking to what Swift’s authors call “Error Handling.” Please read my first, introductory article in this series if you haven’t already. I can’t emphasize enough why you need to use error checking. Remember my discussion of app quality, user intolerance of buggy apps, and the huge amount of choices consumers have? The quality of your apps represents your reputation. Do you want a bad reputation when customers choose apps largely based on reviews? They can and will vote with their fingers (and wallets) and find other apps if they don’t like your app(s). Download my Xcode 9 playground so you can follow along with my discussion, run the code, and experiment with my Swift code by making your own changes.


This article is also a sequel to a tutorial I wrote on “iOS file management with FileManager in protocol-oriented Swift 4,” where I promised to add error checking to my code. iOS file management code absolutely needs error checking because of the nature of file manipulation:

… consider the task of reading and processing data from a file on disk. There are a number of ways this task can fail, including the file not existing at the specified path, the file not having read permissions, or the file not being encoded in a compatible format. Distinguishing among these different situations allows a program to resolve some errors and to communicate to the user any errors it can’t resolve.

“ERROR HANDLING” IN SWIFT 4

Reading Apple’s “The Swift Programming Language (Swift 4.1)” documentation, you’d think that “Error Handling” is the only way to deal with runtime errors. While it isn’t by far the only way to deal with errors, it is an important language feature. I’ll show you how to unleash Swift’s many other capabilities for error checking as this series of tutorials proceeds.

Overview of the Error Handling process

In Swift Error Handling, you mark methods that are capable of generating serious errors with the throws keyword. The definition of “serious” is subjective and up to you the programmer. You then must write code in your throws method (or block) that detects an unexpected condition, initializes an Error object with information describing the condition, and does a throw of the Error. Any other method that calls a method marked with the throws keyword must be able to handle a thrown Error. The calling method should wrap its call to a throws-marked method in a do try construct and either 1) do a catch and handle the Error or 2) do a catch and pass the Error off to the next caller in the call stack by doing a throw of the Error.

Note that you can catch or create and throw NSError objects for interoperability with Objective-C.

Some Apple SDKs still throw traditional exceptions in the form of NSException. Swift’s authors seem to be pushing towards use of Error Handling as the standard for error checking. Discussing exception handling is beyond the scope of this article.

When using an Apple SDK like Foundation, the authors of that code get to decide what constitutes “serious” and you must deal with any Error or NSError instances that are thrown. (You’ll want to at least try to handle NSException, too, if any are thrown while your app is running; if you don’t handle exceptions, your app will crash.)

Error Handling with do try catch

Earlier, I mentioned that if some block of code is marked as throws, you must handle it. You may ask yourself, “Well, how the heck should I know when to handle an Error or NSError? Do I look up every API call I make in the documentation?”

Thankfully, you don’t have to look up every call in every block of code that you write. The Swift compiler will tell you if you need to handle an Error or NSError.

Watch what happens when I try to create an instance of the FileHandle class, a member of Foundation. I start by writing the following statement…

… and the compiler flags it with the error message, “Call can throw, but it is not marked with ‘try’ and the error is not handled” as shown here:

That tells me a whole lot. “Call can throw, but it is not marked with ‘try'” means that init(forReadingFrom:) is marked with the throws and is capable of reporting an Error or NSError.

But wait a minute. What about the last part of that error message; the thing about ‘and the error is not handled’?” Humor me and let me just add the try for now:

You see that we’re not quite finished yet. I’m not yet doing anything if an error is thrown. I’m not handling the error. That’s where Swift’s do try catch syntax comes into the picture. Here’s what I’d do in the simplest and broadest case:

That catch allows me to handle anything that was thrown, but I wouldn’t get any detailed information describing the error. This is a “catch all” methodology, and you’ll generally see more specific catch clauses, like those shown below, but I can’t miss anything thrown using this broad methodology for error capturing.

Now I’ll specifically handle an Error. Notice my comment about leaving out the cast to Error. You should be able to figure that one out yourself. Note also that localizedDescription is the only information I can get from Error:

Because the explanation for “error” given on the documentation page for init(forReadingFrom:) states that “If an error occurs, upon return contains an NSError object that describes the problem,” a more precise form of the previous code block would be:

NSError provides more information in this case — and I want to look for an NSError if I want to get information about the problem that occurred.

Watch what the compiler tells me if I try to catch an Error and an NSError:

#ad

The compiler is telling me that my catch for NSError is redundant and will be ignored. If I switch the order of the catch statements, everything’s OK because I assume that the Swift compiler is looking to handle the most specific errors first and then catch more general ones later:

Suppose I want “pass the buck,” so to speak, to someone else so they can handle the error? I can propagate the error up the call stack to the method that called my FileHandle-related code:

Some code is capable of throwing different Error type instances, each one specifically tailored to the type of problem that occurred. In this case, you may end up with a long list of catch clauses. You can see an example in Apple’s documentation.

Finally, here’s an example of multiple catch statements, meant to be able to capture detailed error information and capture all error possibilities:

Error Handling versus exception handling

Just because Swift Error Handling looks like something called “exception handling” in languages like Objective-C and C++, it isn’t. When an error is detected and a developer throws an exception in one of these languages, something called “stack unwinding” occurs (see also here), which is a resource intensive process. Don’t confuse Swift’s Error Handling with exception handling:

Error handling in Swift resembles exception handling in other languages, with the use of the try, catch and throw keywords. Unlike exception handling in many languages–including Objective-C–error handling in Swift does not involve unwinding the call stack, a process that can be computationally expensive. As such, the performance characteristics of a throw statement are comparable to those of a return statement.

This is a big difference and I urge you to spend a little time researching exception handling in other languages and comparing it to Swift’s Error Handling. This difference constitutes a good reason for you to take advantage of Swift’s Error Handling: it’s convenient and efficient.

The problem with NSException

When I encounter an NSException thrown from some Objective-C API, I can’t handle it unless I jump through hoops. You can examine several suggested courses of action here and here.

In Swift, I can’t wrap an attempt to access an item of an Array at a specific index in a do-try-catch. If my code happens to attempt accessing an Array index which doesn’t exist, I get an NSException of “Fatal error: Index out of range.”

I don’t know why Swift’s authors haven’t addressed this issue.

SWIFT 4 CODE FOR ERROR HANDLING

You should be able to master Swift’s Error Handling after walking through the next example.

Creating an Error and reporting it

What’s the use of going through all the rigamarole of detecting erroneous conditions and flagging them if you don’t know the who, what, when, where, how, and why the error occurred? Swift provides an elegant mechanism for you to capture as much information as you want when an error occurs. You create a struct type and use value semantics designed to capture information describing the error condition.

From Apple’s documentation:

In Swift, errors are represented by values of types that conform to the Error protocol. This empty protocol indicates that a type can be used for error handling.

Swift enumerations are particularly well suited to modeling a group of related error conditions, with associated values allowing for additional information about the nature of an error to be communicated.

Here’s my tried and tested FileSystemError type:

It’s pretty powerful. It lets me group similar errors into categories and use those classifications throughout my code to, for example, label erroneous conditions or make decisions based on those labels. I can get a human readable description of an instance of my error type, making debugging very straightforward. Remember that “Structure types automatically receive a memberwise initializer if they do not define any of their own custom initializers.” Whenever I need to describe a FileSystemError, Xcode’s autocomplete requires me to fill in all values (click on the image to enlarge it):


iOS file management code with Error Handling

Let me walk you through an example of implementing Error Handling for one method. First I’ll show you the raw code, then I’ll explain it. I’m going to describe my code in series of steps — steps that are also comments in my code shown below. So as you read the code in this section, please refer to the corresponding step explanations in the next section.

I added the readBytes method shown below to the original file management code I wrote, displayed, and explained in my article on “iOS file management with FileManager in protocol-oriented Swift 4.”

Error Handling step explanations

While reading each of the following steps, refer to the correspondingly numbered comments in my code above.

STEP 0: Mark the method as throws.

STEP 1: Create a full path to the file from which I want to read.

STEP 2: The init(forReadingFrom:) initializer for FileHandle is marked as throws:

So wrap it in a do try catch block. My do is a bit broad in scope, but possibly wrapping multiple calls in multiple do clauses in the same method is unreadable to me.

STEP 3: try to create a FileHandle instance for reading from a file using the init(forReadingFrom:) initializer.

STEP 4: Wrap the closeFile() call for the file (handle) I just opened in a defer statement block so that the file will be closed “no matter how program control is transferred” out of my readBytes method. Even if an exception is thrown, the file, a valuable resource, will not be left open. What would’ve happened if I had placed the call to closeFile() towards the end of my readBytes method? Suppose further that I made many calls to my readBytes method and they all failed with a throw? Files are valuable resources.

STEP 5: Find the size of the file in bytes using the handle’s availableData property so I can determine how many bytes I can safely read.*

STEP 6: Make sure the developer doesn’t read outside the bounds of the file’s size. Notice I’m not throwing an exception here. I’m showing you that you have choices when dealing with errors. This is a judgement call. I could say that a bounds mistake while reading can be handled by returning nil to the caller. The caller can then decide what to do when nil is returned. Conversely, Apple considers the failure of a FileHandle initializer to be worthy of an Error. Why not just return nil? I believe that Swift’s authors are pushing the language to be more expressive and that marking methods with throws conveys information more immediately than does having to look up the meaning of a return value, if one was used.

STEP 7: Read the data from the file. Call seek(toFileOffset:) to move the file pointer to the position in the file at which the developer wishes to start reading. Call readData(ofLength:) to grab as many bytes as the developer requested. Finally, convert the data chunk read from file into a string encoded as UTF-8. That string is returned to the caller. Remember that the file we’re reading from will be closed by the defer statement I discussed in STEP 4.

STEP 8: If an exception is thrown in STEP 3, it is caught here in the catch clause. Notice that I built a very precise error message by using the FileSystemError type’s initializer. I’m able to get details like the method name, file name, and line number in which the exception occurred using the little-known but well-documented Swift “Literal Expression” list.

NOTE: In STEP 8, I could’ve caught the NSError thrown by the init(forReadingFrom:) method for FileHandle, but I wanted to show you how to create your own custom Error instances.

#ad

* Note that availableData, and several other FileHandle methods and properties, are not marked with throws, but some of their documentation pages state that, for example, “This method raises NSFileHandleOperationException if attempts to determine the file-handle type fail or if attempts to read from the file or channel fail.” What does Apple mean about “this method?” The top of the page for availableData is clearly marked as “Instance Property.” There’s a way to clarify this kind of questionable documentation. I do so by using the Swift compiler.

I wrap a questionable property or method like this in a do try catch block and watch for compiler output:

Notice that the line

is flagged with the warning “No calls to throwing functions occur within ‘try’ expression.”

RUNNING MY CODE

I’ll first show you examples of my code running successfully and then show you how I deal with exceptions.

Creating some files to manipulate

Let me first use my AppFile class to create some files, one of which I’ll read to obtain some data:

Here are the file writes in action:

Reading from file without problems

Here’s the code for reading from one of the files I just wrote out:

Here’s the file read in action — and a still image of the console output just in case you have trouble keeping up with the ani-GIF:

Problem while reading from file

Let me introduce some code into my readBytes function that will throw a FileSystemError. I won’t go into detail as to how I induced an error. Just let me show you where I created several statements that throw. Refer to STEP 7 in my readBytes. I’ll call a couple of lines of code that throw in this section of my method (notice my comment just after the opening brace of my else clause):

I could have dealt with the Error where it occurred in readBytes, but instead I’m going to propagate the error up the call stack until I want to deal with the Error. This is the beauty of Error Handling. I can deal with an error deep down inside an SDK. I can deal with it in intermediate-level code that serves as an interface to an SDK, without informing the user as she/he may not need to know about it. Or I can deal with an error at, for example, the UIViewController level and inform the user directly because the error is so severe as to render an app useless.

So I induce the error so that STEP 8 in my readBytes method is invoked. I collect information about a thrown error and re-throw it to the caller, like so:

The calling method, read(at:), in my AppFile instance does a catch of the thrown Error, and does a throw up to the next block of code in the call stack, as shown here:

Finally, the code that started the whole file read process handles the error by calling my static handle(error:) method of my FileSystemError Swift structure:

Here’s the output to console as the error is caught, both as it happened and as a snapshot:

CONCLUSION

Please again review my introductory remarks on error checking in Swift. Today, we knocked out the topics on “handling Swift-defined and Objective-C-defined instances of Error and NSError, respectively,” “defining my own error types using the Error protocol,” and “using the defer construct to perform mandatory cleanup that would otherwise get skipped because an Error is thrown,” but we still need to go over:

  • the absence of value, nil;
  • expressions like if let;
  • constructs like guard;
  • creating failable initializers (init?); and,
  • using best practices during design to avoid having an overbearing amount of error handling code in my projects.
#ad

Author: Andrew Jaffee

Avid and well-published author, software engineer, designer, and developer, now specializing in iOS mobile app development in Objective-C and Swift, but with a strong background in C#, C++, .NET, JavaScript, HTML, CSS, jQuery, SQL Server, MySQL, Oracle, Agile, Test Driven Development, Git, Continuous Integration, Responsive Web Design, blah, blah, blah ... Did I miss any fad-based catch phrases? My brain avatar was kindly provided by https://icons8.com under a Creative Commons Attribution-NoDerivs 3.0 Unported license.

2 thoughts on “Controlling chaos: Error Handling in Swift 4 with do, try, catch, defer, throw, throws, Error, and NSError”

  1. Might be worth commenting that most of this goes out the window when writing unit tests. An XCTFail( ) simply aborts execution of the current function, with no opportunity to catch any exception, and deferred statements are not executed either.

    1. We don’t strip out error handling so that unit tests will work, rather we use several different methods of testing on a case-by-case basis. We use our own flavor of test-driven development (TDD), we use XCTAssertThrowsError(_:_:file:line:_:) for expressions that throw errors, we use human testing, and we use unit testing. For cases like defer, we depend on TDD and human testing — but we’re certainly not going to just put a bunch of try! statements in our production code.

Leave a Reply

Your email address will not be published. Required fields are marked *