In a perfect world, a running iOS app would never encounter an error. The reality, however, is that it is impossible to guarantee that an error of some form or another will not occur at some point during the execution of the app. It is essential, therefore, to ensure that the code of an app is implemented such that it gracefully handles any errors that may occur. Since the introduction of Swift 2, the task of handling errors has become much easier for the iOS app developer.
This chapter will cover the handling of errors using Swift and introduce topics such as error types, throwing methods and functions, the guard and defer statements and do-catch statements.
Understanding Error Handling
No matter how carefully Swift code is designed and implemented, there will invariably be situations that are beyond the control of the app. An app that relies on an active internet connection cannot, for example, control the loss of signal on an iPhone device, or prevent the user from enabling “airplane mode”. What the app can do, however, is to implement robust handling of the error (for example displaying a message indicating to the user that the app requires an active internet connection to proceed).
There are two sides to handling errors within Swift. The first involves triggering (or throwing) an error when the desired results are not achieved within the method of an iOS app. The second involves catching and handling the error after it is thrown by a method.
When an error is thrown, the error will be of a particular error type which can be used to identify the specific nature of the error and to decide on the most appropriate course of action to be taken. The error type value can be any value that conforms to the Error protocol.
You are reading a sample chapter from an old edition of iOS App Development Essentials. Purchase the fully updated iOS 18 App Development Essentials book. The full book contains 71 chapters, over 612 pages of in-depth information, downloadable source code, and access to over 50 SwiftUI knowledge test quizzes. |
In addition to implementing methods in an app to throw errors when necessary, it is important to be aware that a number of API methods in the iOS SDK (particularly those relating to file handling) will throw errors which will need to be handled within the code of the app.
Declaring Error Types
As an example, consider a method that is required to transfer a file to a remote server. Such a method might fail to transfer the file for a variety of reasons such as there being no network connection, the connection being too slow or the failure to find the file to be transferred. All these possible errors could be represented within an enumeration that conforms to the Error protocol as follows:
enum FileTransferError: Error {
case noConnection
case lowBandwidth
case fileNotFound
}
Code language: Swift (swift)
Once an error type has been declared, it can be used within a method when throwing errors.
Throwing an Error
A method or function declares that it can throw an error using the throws keyword. For example:
func transferFile() throws {
}
Code language: Swift (swift)
In the event that the function or method returns a result, the throws keyword is placed before the return type as follows:
You are reading a sample chapter from an old edition of iOS App Development Essentials. Purchase the fully updated iOS 18 App Development Essentials book. The full book contains 71 chapters, over 612 pages of in-depth information, downloadable source code, and access to over 50 SwiftUI knowledge test quizzes. |
func transferFile() throws -> Bool {
}
Code language: Swift (swift)
Once a method has been declared as being able to throw errors, code can then be added to throw the errors when they are encountered. This is achieved using the throw statement in conjunction with the guard statement. The following code declares some constants to serve as status values and then implements the guard and throw behavior for the method:
let connectionOK = true
let connectionSpeed = 30.00
let fileFound = false
enum FileTransferError: Error {
case noConnection
case lowBandwidth
case fileNotFound
}
func fileTransfer() throws {
guard connectionOK else {
throw FileTransferError.noConnection
}
guard connectionSpeed > 30 else {
throw FileTransferError.lowBandwidth
}
guard fileFound else {
throw FileTransferError.fileNotFound
}
}
Code language: Swift (swift)
Within the body of the method, each guard statement checks a condition for a true or false result. In the event of a false result, the code contained within the else body is executed. In the case of a false result, the throw statement is used to throw one of the error values contained in the FileTransferError enumeration.
Calling Throwing Methods and Functions
Once a method or function is declared as throwing errors, it can no longer be called in the usual manner. Calls to such methods must now be prefixed by the try statement as follows:
try fileTransfer()
Code language: Swift (swift)
Understanding Error Handling in Swift 5 In addition to using the try statement, the call must also be made from within a do-catch statement to catch and handle any errors that may be thrown. Consider, for example, that the fileTransfer method needs to be called from within a method named sendFile. The code within this method might be implemented as follows:
func sendFile() -> String {
do {
try fileTransfer()
} catch FileTransferError.noConnection {
return("No Network Connection")
} catch FileTransferError.lowBandwidth {
return("File Transfer Speed too Low")
} catch FileTransferError.fileNotFound {
return("File not Found")
} catch {
return("Unknown error")
}
return("Successful transfer")
}
Code language: Swift (swift)
The method calls the fileTransfer method from within a do-catch statement which, in turn, includes catch conditions for each of the three possible error conditions. In each case, the method simply returns a string value containing a description of the error. In the event that no error was thrown, a string value is returned indicating a successful file transfer. Note that a fourth catch condition is included with no pattern matching. This is a “catch all” statement that ensures that any errors not matched by the preceding catch statements are also handled. This is required because do-catch statements must be exhaustive (in other words constructed so as to catch all possible error conditions).
You are reading a sample chapter from an old edition of iOS App Development Essentials. Purchase the fully updated iOS 18 App Development Essentials book. The full book contains 71 chapters, over 612 pages of in-depth information, downloadable source code, and access to over 50 SwiftUI knowledge test quizzes. |
Swift also allows multiple matches to be declared within a single catch statement, with the list of matches separated by commas. For example, a single catch declaration could be used to handle both the noConnection and lowBandwidth errors as follows:
func sendFile() -> String {
do {
try fileTransfer()
} catch FileTransferError.noConnection, FileTransferError.lowBandwidth {
return("Connection problem")
} catch FileTransferError.fileNotFound {
return("File not Found")
} catch {
return("Unknown error")
}
return("Successful transfer")
}
Code language: Swift (swift)
Accessing the Error Object
When a method call fails, it will invariably return an Error object identifying the nature of the failure. A common requirement within the catch statement is to gain access to this object so that appropriate corrective action can be taken within the app code. The following code demonstrates how such an error object is accessed from within a catch statement when attempting to create a new file system directory:
do {
try filemgr.createDirectory(atPath: newDir,
withIntermediateDirectories: true,
attributes: nil)
} catch let error {
print("Error: \(error.localizedDescription)")
}
Code language: Swift (swift)
Disabling Error Catching
A throwing method may be forced to run without the need to enclose the call within a do-catch statement by using the try! statement as follows:
try! fileTransfer
Code language: Swift (swift)
In using this approach we are informing the compiler that we know with absolute certainty that the method call will not result in an error being thrown. In the event that an error is thrown when using this technique, the code will fail with a runtime error. As such, this approach should be used sparingly.
Using the defer Statement
The previously implemented sendFile method demonstrated a common scenario when handling errors. Each of the catch clauses in the do-catch statement contained a return statement that returned control to the calling method. In such a situation, however, it might be useful to be able to perform some other task before control is returned and regardless of the type of error that was encountered. The sendFile method might, for example, need to remove temporary files before returning. This behavior can be achieved using the defer statement.
You are reading a sample chapter from an old edition of iOS App Development Essentials. Purchase the fully updated iOS 18 App Development Essentials book. The full book contains 71 chapters, over 612 pages of in-depth information, downloadable source code, and access to over 50 SwiftUI knowledge test quizzes. |
The defer statement allows a sequence of code statements to be declared as needing to be run as soon as the method returns. In the following code, the sendFile method has been modified to include a defer statement:
func sendFile() -> String {
defer {
removeTmpFiles()
closeConnection()
}
do {
try fileTransfer()
} catch FileTransferError.noConnection {
return("No Network Connection")
} catch FileTransferError.lowBandwidth {
return("File Transfer Speed too Low")
} catch FileTransferError.fileNotFound {
return("File not Found")
} catch {
return("Unknown error")
}
return("Successful transfer")
}
Code language: Swift (swift)
With the defer statement now added, the calls to the removeTmpFiles and closeConnection methods will always be made before the method returns, regardless of which return call gets triggered.
Summary
Error handling is an essential part of creating robust and reliable iOS apps. Since the introduction of Swift 2 it is now much easier to both trigger and handle errors. Error types are created using values that conform to the Error protocol and are most commonly implemented as enumerations. Methods and functions that throw errors are declared as such using the throws keyword. The guard and throw statements are used within the body of these methods or functions to throw errors based on the error type.
A throwable method or function is called using the try statement which must be encapsulated within a do-catch statement. A do-catch statement consists of an exhaustive list of catch pattern constructs, each of which contains the code to be executed in the event of a particular error being thrown. Cleanup tasks can be defined to be executed when a method returns through the use of the defer statement.