We ended the last section by examining some sample code after it had been made production-quality by adding correct error handling techniques. In this section, we'll dig into the most common Swift error handling techniques, which will help ensure all the code you develop in Swift will be robust and of high quality.
Swift supports many of the same error handling techniques available in other object-oriented languages, such as C++, Java, and C#. Functions—either your own or standard library functions—often return error codes as integers, error types, and Boolean variables. In addition, Swift provides exception handling using the do…catch
construction, which is functionally equivalent to the try…catch
construction used in many other languages.
Most modern languages have exception handling features that allow code to throw exceptions from an inner scope that can be caught in an outer scope. In Swift, this pattern is implemented using the do…catch
structure.
You'll very often use the Swift do…catch
structure when calling underlying Apple frameworks to do data processing or file access work on your behalf. Catching exceptions can help bubble up highly detailed error information to your code.
The following code declares a block that calls a function decode, which may throw an exception of type Error
:
do { let userObject = try decode() print(userObject.name) } catch let error { print(error) }
The important thing to note is that the code in between do
and catch
doesn't explicitly check for an error. It simply instructs the decode
function to try to complete successfully. In the event that decode
encounters an error, the remainder of the do block will be skipped and the catch
block will receive the thrown Error
object, assigning it to the local variable error
.
In practice, a function that throws an exception may throw one of several more specific exceptions, depending on what went wrong.
The do…catch
construction allows you to catch more than one exception type. This works almost identically to constructing a switch
statement with multiple case code blocks.
Multiple catch
blocks provide the program with more specific information about the cause of the decoding error, if available, for example:
func decodeWithException() { if let data = jsonText.data(using: String.Encoding.utf8) { let decoder = JSONDecoder() do { let userObject = try decoder.decode(UserInfo.self, from: data) print("User decoded form JSON: \(userObject)") } catch let DecodingError.typeMismatch(_, context) { print("Type Mismatch Error: \(context.debugDescription)") } catch let DecodingError.dataCorrupted(context) { print("Decoding Error: \(context.debugDescription)") } catch let error { print(error.localizedDescription) } print("program always continues from this point.") } }
What if you didn't want to catch an exception, but wanted your program to continue even when an exception is thrown?
By using the try?
keyword (that is, try
with a question mark after it), we can ask Swift to try to run code that may throw an exception, and return the result as an optional variable. In this case, if an exception is thrown, the returned optional will be nil
; if no exception is thrown, the optional will contain the value the function would normally return, for example:
do { let userObject = try? decode() print(userObject?.name) }
In this case, if the decode function throws an exception, the userObject
optional will be nil
, and the print(userObject.name
) line will not be executed. Because the action taken if an exception is thrown is to assign nil to the variable on the left-hand side of the equal sign, it's no longer necessary to wrap the decode call in the do…catch
block.
The guard statement is most commonly used at the top of a function body to validate that the data the function will use to complete its task is in an expected state. In this sense, the guard statement acts as a guard at the gate—checking the contents of inputs to the function before they're allowed in.
In early versions of Swift, we didn't have the guard
statement, and it was common to implement functions structured like the following:
func printAddress1(zipCode: String?, countryCode: String?, areaCode: String?) -> Bool { if let zip = zipCode, let country = countryCode, let area = areaCode { if zip.count != 5 { return } if country.count != 2 { return } if area.count != 3 { return } print("\(zip), \(country), \(area)") } }
While this function isn't too difficult to follow, it can become confusing for the reader where the ending brace of the if let { }
block ends. Developers would frequently reduce the editor font to a tiny size to try to make out where in the sequence of ending braces the close of the original error checking if let block ended!
The guard keyword is effectively a clearer version of this structure—moving the closing braces of validations together in neat code blocks. An equivalent function using the guard syntax is as follows:
func printAddress(zipCode: String?, countryCode: String?, areaCode: String?) { guard let zip = zipCode, zip.count == 5 else { return } guard let country = countryCode, country.count == 2 else { return } guard let area = areaCode, area.count == 3 else { return } print("\(zip), \(country), \(area)") }
In the second version, the guard
statement makes the code more readable, and moves all the state-checking code to the beginning of the function where it can be easily reviewed and understood.
We have reached the end of this section. Here, we focused on error handling and exception handling, as implemented in Swift. To reiterate, Swift uses do…catch
instead of try…catch
and also allows us to use multiple catch
blocks.
Exception handling, as the name implies, is an error handling technique that enables you to let the Swift compiler know what errors you expect, and provide a way to listen for them if they occur while your program is running. We'll now apply exception handling in one of the most common use cases for application developers—parsing data structures from JSON into application data structures.
Use an Xcode playground to practice catching an exception while parsing a JSON string into a custom data structure—a very common task in any application development work that involves integration with web services.
ExceptionHandling.playground
.import Foundation
struct UserInfo : Codable { var name: String var email: String var userId: String }
decodeJson
function to decode a JSON string:func decodeJson(jsonText: String) { if let data = jsonText.data(using: String.Encoding.utf8) { let decoder = JSONDecoder() do { let userObject = try decoder.decode(UserInfo.self, from: data) print("User decoded form JSON: \(userObject)") } catch let error { print(error.localizedDescription) } } print("program always continues from this point.") }
decodeJson
function with a data string that almost correctly matches the expected data structure keys (the name field has the wrong case):decodeJson(jsonText : "{ \"Name\" : \"John Smith\", \"email\" : \"john@smith.com\", \"userId\" : \"jsmith\"}")
Because the jsonText
data is not in the correct format (the name field cannot begin with an uppercase letter), the decoder.decode
function throws an exception. The exception is caught in the catch
block, reporting an error. You eliminate the exception by changing the case of the name field in the jsonText
string.