In this tutorial, the third in a series of tutorials, we’re going to finish the arduous topic of looking for unexpected values, events, and conditions that arise during program execution, using a technique I like to call “error checking.” Today, I’ll concentrate on nil
values, optionals, optional binding, the guard
statement, failable initializers, and finally, give you some advice about keeping your error checking code consistent, for example, when to use Swift “Error Handling” or when just to return true/false
or use guard
statements.
Remember that in the first tutorial, I concentrated on why error checking is so important. We talked about software complexity (chaos), measuring complexity, and how unchecked complexity can easily lead to buggy software that will put you out of business in a heartbeat. Remember that in the second tutorial, I showed you how to implement error checking in terms of what Swift’s authors call “Error Handling,” i.e., the use of do
, try
, catch
, throw
, throws
, try!
, try?
, defer
, Error
, NSError
…
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.
With today’s discussion, we’ll have rounded out the list of tools for detecting in-execution problems:
- the absence of value,
nil
; - expressions like
if let
; - constructs like
guard
; - handling Swift-defined and Objective-C-defined instances of
Error
andNSError
, respectively; - defining my own error types using the
Error
protocol; - creating failable initializers (
init?
); and, - using the
defer
construct to perform mandatory cleanup that would otherwise get skipped because anError
is thrown.
REFACTORING MY CODE TO USE ERROR CHECKING
Let’s start refactoring my protocol-oriented iOS file management code to use error checking. I originally presented and explained the code here and made it available for download from GitHub here. I originally left out error checking so the code would be readable and you could concentrate on the file system and protocol-oriented programming. While the adding of error checking in this section may seem obvious and or trivial to some of you, so be it. After working in and consulting in the field of computer science for 30 years, I constantly have seen, and constantly do to this day see, thousands of lines of code written without error checking. It seems that many developers and/or development boutiques are happy to eat and waste thousands (or more) of dollars and thousands (or more) of hours — and lead very stressful lives. They waste time and money which could be much better well spent on positive activities like continuing education, market research, and exploring new product features rather than taking a bit of time to be fastidious and write good, solid code.
I write this blog in the hopes of reaching those willing to succeed and willing to become the best.
Error Handling
I covered Swift Error Handling in-depth in my tutorial “Controlling chaos: Error Handling in Swift 4 with do, try, catch, defer, throw, throws, Error, and NSError.” With this technique, you can gather much information about an error, such the file name, line number, and function name in which it occurred, as well as the error’s reason.
Optionals, nil
, and if let
One convention adopted by Swift’s designers is the almost axiomatic assumption that failure is associated with nil
, “a valueless state.” Another reason for supporting nil
is convenience: a certain operation sometimes requires specific information, like in the form of an argument, and at other times doesn’t need that information. So we declare that argument as an optional and when not needed, let it contain nil
during app execution. According to Apple:
You use optionals in situations where a value may be absent. An optional represents two possibilities: Either there is a value, and you can unwrap the optional to access that value, or there isn’t a value at all.
Let’s refactor my method for getting the iOS Documents/
directory. Apple states that you should “Use this directory to store user-generated content.”
Here’s my original code with error checking left out for didactic purposes (I wanted you to concentrate on base functionality, not distract you with other details):
1 2 3 4 5 6 7 8 |
... extension AppDirectoryNames { func documentsDirectoryURL() -> URL { return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! } ... |
Notice that I force unwrapped an optional for expediency (first!
), though that act in production would be fraught with danger. Let’s first handle errors with optional binding, defined by Apple thusly:
You use optional binding to find out whether an optional contains a value, and if so, to make that value available as a temporary constant or variable. Optional binding can be used with if and while statements to check for a value inside an optional, and to extract that value into a constant or variable, as part of a single action.
Here’s a simple example of optional binding using if let
in a method:
1 2 3 4 5 6 7 8 9 10 11 |
func ifLetCoordinate(x: Int?, y: Int?, z: Int?) -> (Int?, Int?, Int?) { if let x = x, let y = y, let z = z { return (x, y, z) } else { return (nil, nil, nil) } } |
Here are a few statements testing my ifLetCoordinate
function. The output from each statement is shown as a comment immediately following that statement:
1 2 3 4 5 6 7 8 9 10 |
let coordinate = ifLetCoordinate(x: 1, y: 1, z: 2) // (.0 1, .1 1, .2 2) print(coordinate) // "(Optional(1), Optional(1), Optional(2))\n" coordinate.0 // 1 coordinate.1 // 1 coordinate.2 // 2 let coordinate0 = ifLetCoordinate(x: 1, y: nil, z: 2) // (nil, nil, nil) |
Back to my file system use case … Before trying to access an optional (i.e., URL?
) that could be nil
and thus crash our app, we unwrap that optional to see if it has a value or has no value:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
... func documentsDirectoryURL() -> URL? { // Chance to unwrap optionals into // variables and constants. if let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { // Have to do everything with unwrapped // variables and constants here. return documentsDirectoryURL } else { // No access to variables and constants. // Some say bailing here is counter- // intuitive. return nil } } ... |
Some have complained that optional binding is a bit awkward, as the optional’s unwrapped value, if not nil
, is only available inside the body of the if let
statement. If you have a method that has to check a bunch of values, and those values happen to be all non-nil
, then you have to squeeze all your functionality into that if let
block.
The fact that the documentsDirectoryURL()
method now returns URL?
warns the developer making use of this function to check the returned URL
before assuming it has a valid value. Notice that my iOS file management code already contains several abstraction mechanisms for the documentsDirectoryURL()
method, wrappers like getURL(for:)
and buildFullPath(forFileName:inDirectory:)
, and that at even a higher level than this API, I’d minimize the need for the developer for having to use optional binding (if let
) over and over again. We’ll talk about that later.
Optionals and guard
Some say the guard
statement or “early exit” is more intuitive than the if let
statement. Here’s my original method refactored for guard
instead of if let
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
... func documentsDirectoryURL() -> URL? { // I can check for all preconditions here... // Chance to unwrap optionals into // variables and constants. guard let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { // If any of the guard conditions fail, // "exit the code block in which the // guard statement appears." return nil } // end guard // If I pass guard, I have all my unwrapped // optionals in variables and constants here. // Can continue on and get some work done. return documentsDirectoryURL } ... |
From Apple:
A guard
statement, like an if
statement, executes statements depending on the Boolean value of an expression. You use a guard
statement to require that a condition must be true in order for the code after the guard
statement to be executed. Unlike an if
statement, a guard
statement always has an else
clause–the code inside the else
clause is executed if the condition is not true.
Here’s a simple example of optional binding using guard
in a method:
1 2 3 4 5 6 7 8 |
func guardCoordinate(x: Int?, y: Int?, z: Int?) -> (Int?, Int?, Int?) { guard let x = x, let y = y, let z = z else { return (nil, nil, nil) } return (x, y, z) } |
Here are a few statements testing my guardCoordinate
function. The output from each statement is shown as a comment immediately following that statement:
1 2 3 4 5 6 7 8 9 10 |
let coordinate1 = guardCoordinate(x: 1, y: 1, z: 2) // (.0 1, .1 1, .2 2) print(coordinate1) // "(Optional(1), Optional(1), Optional(2))\n" coordinate1.0 // 1 coordinate1.1 // 1 coordinate1.2 // 2 let coordinate2 = guardCoordinate(x: nil, y: 1, z: 2) // (nil, nil, nil) |
Here’s a simple example of optional binding and tests for non-zero values using guard
and the >
operator in a method:
1 2 3 4 5 6 7 8 |
func guardCoordinateNonNegative(x: Int?, y: Int?, z: Int?) -> (Int?, Int?, Int?) { guard let x = x, x > 0, let y = y, y > 0, let z = z, z > 0 else { return (nil, nil, nil) } return (x, y, z) } |
Notice that I separated the optional binding and greater than operator tests with commas. Here are a few statements testing my guardCoordinateNonNegative
function. The output from each statement is shown as a comment immediately following each statement:
1 2 3 4 5 6 |
let coordinate3 = guardCoordinateNonNegative(x: -1, y: 1, z: 2) // (nil, nil, nil) let coordinate4 = guardCoordinateNonNegative(x: 1, y: 1, z: nil) // (nil, nil, nil) let coordinate5 = guardCoordinateNonNegative(x: 2, y: 2, z: 3) // (.0 2, .1 2, .2 3) |
Back to my initial file system guard
example above, if the guard condition is true, i.e., the documentsDirectoryURL
value is not nil
and contains a valid URL
, all the code after the guard
closing brace (} // end guard
) is executed… and all the optionals I unwrapped using optional binding in the guard
statement are available until the closing brace of my documentsDirectoryURL()
method.
If any of the guard
conditions fail, its else
statement is executed and that “branch must transfer control to exit the code block in which the guard statement appears.” So I get all my error checking over with first, and if everything’s cool, I get to execute code with my unwrapped optionals. As Apple puts it:
Using a guard
statement for requirements improves the readability of your code, compared to doing the same check with an if
statement. It lets you write the code that’s typically executed without wrapping it in an else block, and it lets you keep the code that handles a violated requirement next to the requirement.
By putting a guard
statement at the beginning of a block like a method, the opportunity is given to check for preconditions and exit almost immediately if some of those conditions are not met. If all conditions are met, the bulk of the the block’s functionality forms the rest of the method, so the issue of having one extra exit point near the top of the method keeps at least the spirit of a method having one entry point and one exit point alive.
Failable initializers
Suppose I write an app that is utterly and completely dependent on my protocol-oriented iOS file management code. In other words, my app is useless without the iOS file system and the capability for individual file manipulation… And/or just suppose I want to make it very simple for a developer to determine if the iOS file system on the host device is working properly and if he/she can easily make use of my iOS file management code? What about creating a class
, struct
, or even enum
that has a failable initializer? From Apple:
It is sometimes useful to define a class, structure, or enumeration for which initialization can fail. This failure might be triggered by invalid initialization parameter values, the absence of a required external resource, or some other condition that prevents initialization from succeeding.
To cope with initialization conditions that can fail, define one or more failable initializers as part of a class, structure, or enumeration definition. You write a failable initializer by placing a question mark after the init keyword (init?
). …
A failable initializer creates an optional value of the type it initializes. You write return nil
within a failable initializer to indicate a point at which initialization failure can be triggered.
In the case of my iOS file management code, the failure I’m looking for would be the lack of an essential resource, the iOS file system (for a variety of reasons, including device hardware problems). After everything we’ve discussed in this tutorial, all the examples I’ve provided, and all the references (hyperlinks) I’ve given you, do I really need to explain the following code? Note that I’ve abbreviated my original protocol-based code for didactic purposes — i.e., so you can concentrate on checking for nil
values and concentrate on how a failable initializer works. Here’s some working code that you could pop into an Xcode playground and see work immediately — and note the failable initializer on line 30:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
func getDocumentsDirectoryURL() -> URL? { if let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { return documentsDirectoryURL } else { return nil } } // end func getDocumentsDirectoryURL() func getLibraryDirectoryURL() -> URL? { if let libraryDirectoryURL = FileManager.default.urls(for: FileManager.SearchPathDirectory.libraryDirectory, in: .userDomainMask).first { return libraryDirectoryURL } else { return nil } } // end func getLibraryDirectoryURL() struct iOSFileSystem { let documentsDirectoryURL: URL let libraryDirectoryURL: URL init?() // FAILABLE INITIALIZER { if let documentsDirectoryURL = getDocumentsDirectoryURL(), let libraryDirectoryURL = getLibraryDirectoryURL() { self.documentsDirectoryURL = documentsDirectoryURL self.libraryDirectoryURL = libraryDirectoryURL } else { return nil } } // Add code for manipulating files here... } // end struct iOSFileSystem if let iOSfileSystem = iOSFileSystem() { print("File system supported.") print("Documents: \(iOSfileSystem.documentsDirectoryURL)") print("Library: \(iOSfileSystem.libraryDirectoryURL)") } else { fatalError("This app cannot function without access to the iOS file system.") } |
Here’s the output (below) from the last 10 lines of sample code (shown immediately above):
1 2 3 |
File system supported. Documents: file:///Users/softwaretesting/Documents/ Library: file:///Users/softwaretesting/Library/ |
Note that this code was run on macOS for simplicity’s sake, but will run fine under iOS.
CONSISTENCY WHEN DESIGNING ERROR CHECKING
I’m going to mention some inconsistencies in Apple’s error checking. I do so not to criticize Apple, but to try to give you some sound advice when designing the error checking you write in your own code.
Consider an operation (method) that depends on what could be relatively easily-expressed erroneous input, like a String
argument that is typed in at the keyboard by the developer, is considered suspect, prone to failure, and thus, if it fails, it returns nil
. A good example is the macOS-based instance method of the FileManager.default
singleton named homeDirectory(forUser:)
, which “Returns the home directory for the specified user,” and that username is specified by some developer typing in the userName
argument. Why only return nil
if, say, the user’s account was deleted by an administrator during app execution, or if the username was misspelled? All’s we’re told is nil
. That doesn’t help a developer understand why her/his attempt to obtain a user’s home directory failed.
Consider another operation (method) that depends upon possibly erroneous input, also a String
argument that is typed by the developer, in the macOS-, iOS-, tvOS-, and watchOS-based instance method of the FileManager.default
singleton named contentsOfDirectory(atPath:)
. If the developer types in a bad value for the path
argument, or, say, the file system is corrupted, this method throws
an Error
(or NSError
). The Apple docs state that “this method returns a nonoptional result and is marked with the throws
keyword to indicate that it throws an error in cases of failure.”
OK, so what’s the difference between the two methods? I can envision use cases in which the failure of either method or both methods could be catastrophic or non-catastrophic to an app’s performance. Did Apple flip a coin? More likely Apple is in the process of living in a very competitive world, getting out software as fast as it can to keep up with competitors, and doing its best to standardize things like error handling over the long term. That’s all speculation.
What should you do? I would advise flexibility. No need to use a jackhammer to drive in a finishing nail; conversely, don’t try to pry up a railroad tie with a simple hammer. When designing straightforward methods whose purpose is singularly well-defined and confined to a very small scope, you can get away with returning nil
if a method fails. When dealing with a mission critical operation that has to be confined to one atomic call, has many possible points of failure, and is complex, mark your method with throws
and, in the case of failure, create an Error
(or NSError
) instance with very specific information about why the method failed, if it fails.
There’s one more possibility worth exploring. What about methods that return a Bool
, true
when succeeding and false
when failing? Understanding true
is easy; understanding false
is not always so easy. Consider another operation (method) that depends upon possibly erroneous input, a String
argument that is typed by the developer, a Data?
argument constructed or obtained by the developer, and a file attributes argument (including FileAttributeKey
) as specified by the developer. The parameters are passed to the macOS-, iOS-, tvOS-, and watchOS-based instance method of the FileManager.default
singleton named createFile(atPath:contents:attributes:)
. If the developer types in a bad value for the path
argument, constructs invalid (like badly encoded) data, creates conflicting file attributes or, say, the file system is corrupted, this method just returns false
. That’s not very helpful, especially if this call crashes on some device on the other side of the world and all the developers get is an email reporting a crash when a user tries to save a file, and of course, no crash log is included.
If you’re writing a method like this one, don’t just return true
or false
, mark your method with throws
and, in the case of failure, create an Error
(or NSError
) instance with very specific information about why the method failed, if it fails.
CONCLUSION
There is one more thing I’d like to discuss eventually: using best practices during design to avoid having an overbearing amount of error handling code in projects. This is quite a broad topic, but I believe I can convey to you my successful techniques for proactive and prophylactic design and coding.
Too many developers want to go straight to the code for writing the app they’ve been tasked with developing. In fact, some would argue that many developers nowadays write code by doing Internet searches, copying code from sites like StackOverflow, and pasting it into their projects.
Yes, there’s a big demand for developers, but don’t ever take anything for granted. Whenever the next economic recession or stock market crash occurs, many of these same people will be out of work. The developers with truly exceptional skills will probably keep their jobs through any dips. The copy and paste developers will be out of work and looking for work and probably not finding work until the next boom when the spigot for seed money for silly startups starts spraying all over the place.
We’re living in an exceptional boom time right now. I’m old enough to remember really rough economic times. Don’t believe the people who say “it’s different this time” because it’s not. There will always be ups and downs.
The question of the day is: Are you a truly talented developer who understands the subtleties of software complexity, like error checking, or are you a disposable copy and paste developer?