Error handling is the process of responding to and recovering from error conditions in your program. Swift provides first-class support for throwing, catching, propagating, and manipulating recoverable errors at runtime.
Some operations aren’t guaranteed to always complete execution or produce a useful output. Optionals are used to represent the absence of a value, but when an operation fails, it’s often useful to understand what caused the failure, so that your code can respond accordingly.
As an example, 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.
Representing and Throwing Errors¶
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. For example, here’s how you might represent the error conditions of operating a vending machine inside a game:
- enum VendingMachineError: Error {
- case invalidSelection
- case insufficientFunds(coinsNeeded: Int)
- case outOfStock
- }
Throwing an error lets you indicate that something unexpected happened and the normal flow of execution can’t continue. You use a throw
statement to throw an error. For example, the following code throws an error to indicate that five additional coins are needed by the vending machine:
- throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
Handling Errors¶
When an error is thrown, some surrounding piece of code must be responsible for handling the error—for example, by correcting the problem, trying an alternative approach, or informing the user of the failure.
There are four ways to handle errors in Swift. You can propagate the error from a function to the code that calls that function, handle the error using a do
—catch
statement, handle the error as an optional value, or assert that the error will not occur. Each approach is described in a section below.
When a function throws an error, it changes the flow of your program, so it’s important that you can quickly identify places in your code that can throw errors. To identify these places in your code, write the try
keyword—or the try?
or try!
variation—before a piece of code that calls a function, method, or initializer that can throw an error. These keywords are described in the sections below.
Note
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 doesn’t 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.
Propagating Errors Using Throwing Functions¶
To indicate that a function, method, or initializer can throw an error, you write the throws
keyword in the function’s declaration after its parameters. A function marked with throws
is called a throwing function. If the function specifies a return type, you write the throws
keyword before the return arrow (->
).
- func canThrowErrors() throws -> String
- func cannotThrowErrors() -> String
A throwing function propagates errors that are thrown inside of it to the scope from which it’s called.
Note
Only throwing functions can propagate errors. Any errors thrown inside a nonthrowing function must be handled inside the function.
In the example below, the VendingMachine
class has a vend(itemNamed:)
method that throws an appropriate VendingMachineError
if the requested item isn’t available, is out of stock, or has a cost that exceeds the current deposited amount:
- struct Item {
- var price: Int
- var count: Int
- }
- class VendingMachine {
- var inventory = [
- «Candy Bar»: Item(price: 12, count: 7),
- «Chips»: Item(price: 10, count: 4),
- «Pretzels»: Item(price: 7, count: 11)
- ]
- var coinsDeposited = 0
- func vend(itemNamed name: String) throws {
- guard let item = inventory[name] else {
- throw VendingMachineError.invalidSelection
- }
- guard item.count > 0 else {
- throw VendingMachineError.outOfStock
- }
- guard item.price <= coinsDeposited else {
- throw VendingMachineError.insufficientFunds(coinsNeeded: item.price — coinsDeposited)
- }
- coinsDeposited -= item.price
- var newItem = item
- newItem.count -= 1
- inventory[name] = newItem
- print(«Dispensing (name)«)
- }
- }
The implementation of the vend(itemNamed:)
method uses guard
statements to exit the method early and throw appropriate errors if any of the requirements for purchasing a snack aren’t met. Because a throw
statement immediately transfers program control, an item will be vended only if all of these requirements are met.
Because the vend(itemNamed:)
method propagates any errors it throws, any code that calls this method must either handle the errors—using a do
—catch
statement, try?
, or try!
—or continue to propagate them. For example, the buyFavoriteSnack(person:vendingMachine:)
in the example below is also a throwing function, and any errors that the vend(itemNamed:)
method throws will propagate up to the point where the buyFavoriteSnack(person:vendingMachine:)
function is called.
- let favoriteSnacks = [
- «Alice»: «Chips»,
- «Bob»: «Licorice»,
- «Eve»: «Pretzels»,
- ]
- func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
- let snackName = favoriteSnacks[person] ?? «Candy Bar»
- try vendingMachine.vend(itemNamed: snackName)
- }
In this example, the buyFavoriteSnack(person: vendingMachine:)
function looks up a given person’s favorite snack and tries to buy it for them by calling the vend(itemNamed:)
method. Because the vend(itemNamed:)
method can throw an error, it’s called with the try
keyword in front of it.
Throwing initializers can propagate errors in the same way as throwing functions. For example, the initializer for the PurchasedSnack
structure in the listing below calls a throwing function as part of the initialization process, and it handles any errors that it encounters by propagating them to its caller.
- struct PurchasedSnack {
- let name: String
- init(name: String, vendingMachine: VendingMachine) throws {
- try vendingMachine.vend(itemNamed: name)
- self.name = name
- }
- }
Handling Errors Using Do-Catch¶
You use a do
—catch
statement to handle errors by running a block of code. If an error is thrown by the code in the do
clause, it’s matched against the catch
clauses to determine which one of them can handle the error.
Here is the general form of a do
—catch
statement:
- do {
- try expression
- statements
- } catch pattern 1 {
- statements
- } catch pattern 2 where condition {
- statements
- } catch pattern 3, pattern 4 where condition {
- statements
- } catch {
- statements
- }
You write a pattern after catch
to indicate what errors that clause can handle. If a catch
clause doesn’t have a pattern, the clause matches any error and binds the error to a local constant named error
. For more information about pattern matching, see Patterns.
For example, the following code matches against all three cases of the VendingMachineError
enumeration.
- var vendingMachine = VendingMachine()
- vendingMachine.coinsDeposited = 8
- do {
- try buyFavoriteSnack(person: «Alice», vendingMachine: vendingMachine)
- print(«Success! Yum.»)
- } catch VendingMachineError.invalidSelection {
- print(«Invalid Selection.»)
- } catch VendingMachineError.outOfStock {
- print(«Out of Stock.»)
- } catch VendingMachineError.insufficientFunds(let coinsNeeded) {
- print(«Insufficient funds. Please insert an additional (coinsNeeded) coins.»)
- } catch {
- print(«Unexpected error: (error).»)
- }
- // Prints «Insufficient funds. Please insert an additional 2 coins.»
In the above example, the buyFavoriteSnack(person:vendingMachine:)
function is called in a try
expression, because it can throw an error. If an error is thrown, execution immediately transfers to the catch
clauses, which decide whether to allow propagation to continue. If no pattern is matched, the error gets caught by the final catch
clause and is bound to a local error
constant. If no error is thrown, the remaining statements in the do
statement are executed.
The catch
clauses don’t have to handle every possible error that the code in the do
clause can throw. If none of the catch
clauses handle the error, the error propagates to the surrounding scope. However, the propagated error must be handled by some surrounding scope. In a nonthrowing function, an enclosing do
—catch
statement must handle the error. In a throwing function, either an enclosing do
—catch
statement or the caller must handle the error. If the error propagates to the top-level scope without being handled, you’ll get a runtime error.
For example, the above example can be written so any error that isn’t a VendingMachineError
is instead caught by the calling function:
- func nourish(with item: String) throws {
- do {
- try vendingMachine.vend(itemNamed: item)
- } catch is VendingMachineError {
- print(«Couldn’t buy that from the vending machine.»)
- }
- }
- do {
- try nourish(with: «Beet-Flavored Chips»)
- } catch {
- print(«Unexpected non-vending-machine-related error: (error)«)
- }
- // Prints «Couldn’t buy that from the vending machine.»
In the nourish(with:)
function, if vend(itemNamed:)
throws an error that’s one of the cases of the VendingMachineError
enumeration, nourish(with:)
handles the error by printing a message. Otherwise, nourish(with:)
propagates the error to its call site. The error is then caught by the general catch
clause.
Another way to catch several related errors is to list them after catch
, separated by commas. For example:
- func eat(item: String) throws {
- do {
- try vendingMachine.vend(itemNamed: item)
- } catch VendingMachineError.invalidSelection, VendingMachineError.insufficientFunds, VendingMachineError.outOfStock {
- print(«Invalid selection, out of stock, or not enough money.»)
- }
- }
The eat(item:)
function lists the vending machine errors to catch, and its error text corresponds to the items in that list. If any of the three listed errors are thrown, this catch
clause handles them by printing a message. Any other errors are propagated to the surrounding scope, including any vending-machine errors that might be added later.
Converting Errors to Optional Values¶
You use try?
to handle an error by converting it to an optional value. If an error is thrown while evaluating the try?
expression, the value of the expression is nil
. For example, in the following code x
and y
have the same value and behavior:
- func someThrowingFunction() throws -> Int {
- // …
- }
- let x = try? someThrowingFunction()
- let y: Int?
- do {
- y = try someThrowingFunction()
- } catch {
- y = nil
- }
If someThrowingFunction()
throws an error, the value of x
and y
is nil
. Otherwise, the value of x
and y
is the value that the function returned. Note that x
and y
are an optional of whatever type someThrowingFunction()
returns. Here the function returns an integer, so x
and y
are optional integers.
Using try?
lets you write concise error handling code when you want to handle all errors in the same way. For example, the following code uses several approaches to fetch data, or returns nil
if all of the approaches fail.
- func fetchData() -> Data? {
- if let data = try? fetchDataFromDisk() { return data }
- if let data = try? fetchDataFromServer() { return data }
- return nil
- }
Disabling Error Propagation¶
Sometimes you know a throwing function or method won’t, in fact, throw an error at runtime. On those occasions, you can write try!
before the expression to disable error propagation and wrap the call in a runtime assertion that no error will be thrown. If an error actually is thrown, you’ll get a runtime error.
For example, the following code uses a loadImage(atPath:)
function, which loads the image resource at a given path or throws an error if the image can’t be loaded. In this case, because the image is shipped with the application, no error will be thrown at runtime, so it’s appropriate to disable error propagation.
- let photo = try! loadImage(atPath: «./Resources/John Appleseed.jpg»)
Specifying Cleanup Actions¶
You use a defer
statement to execute a set of statements just before code execution leaves the current block of code. This statement lets you do any necessary cleanup that should be performed regardless of how execution leaves the current block of code—whether it leaves because an error was thrown or because of a statement such as return
or break
. For example, you can use a defer
statement to ensure that file descriptors are closed and manually allocated memory is freed.
A defer
statement defers execution until the current scope is exited. This statement consists of the defer
keyword and the statements to be executed later. The deferred statements may not contain any code that would transfer control out of the statements, such as a break
or a return
statement, or by throwing an error. Deferred actions are executed in the reverse of the order that they’re written in your source code. That is, the code in the first defer
statement executes last, the code in the second defer
statement executes second to last, and so on. The last defer
statement in source code order executes first.
- func processFile(filename: String) throws {
- if exists(filename) {
- let file = open(filename)
- defer {
- close(file)
- }
- while let line = try file.readline() {
- // Work with the file.
- }
- // close(file) is called here, at the end of the scope.
- }
- }
The above example uses a defer
statement to ensure that the open(_:)
function has a corresponding call to close(_:)
.
Note
You can use a defer
statement even when no error handling code is involved.
Creating custom error types is an important skill when designing clear APIs for iOS and macOS applications. This post presents examples for creating, extending, throwing, and catching custom error types in Swift:
- Create A Custom Error And Conform To The Error Type
- Extend A Custom Error
- description for Custom Errors Using CustomStringConvertible
- localizedDescription For Custom Errors Using LocalizedError
- Throw Custom Errors
- Catch Custom Errors
Create A Custom Error And Conform To The Error Type
To create a custom error, create an enum
in Swift that conforms to the Error
protocol. Each case of the enum represents a unique error that can be thrown and handled:
enum CustomError: Error {
// Throw when an invalid password is entered
case invalidPassword
// Throw when an expected resource is not found
case notFound
// Throw in all other cases
case unexpected(code: Int)
}
Extend A Custom Error
Like all Swift types, new custom error types you create can be extended to add computed properties and functions. In this example, the isFatal
computed property is added that can be used to determine if the error is recoverable:
extension CustomError {
var isFatal: Bool {
if case CustomError.unexpected = self { return true }
else { return false }
}
}
description for Custom Errors Using CustomStringConvertible
Custom errors implemented in Swift can have custom descriptions for each error. To add a description to a new error type, extend the custom error to conform to CustomStringConvertible
and add a property description
:
// For each error type return the appropriate description
extension CustomError: CustomStringConvertible {
public var description: String {
switch self {
case .invalidPassword:
return "The provided password is not valid."
case .notFound:
return "The specified item could not be found."
case .unexpected(_):
return "An unexpected error occurred."
}
}
}
localizedDescription For Custom Errors Using LocalizedError
New custom errors you create in Swift can also have localized custom descriptions for each error. To add a localized description to a new error type, extend the custom error to conform to LocalizedError
and add a property errorDescription
:
// For each error type return the appropriate localized description
extension CustomError: LocalizedError {
public var errorDescription: String? {
switch self {
case .invalidPassword:
return NSLocalizedString(
"The provided password is not valid.",
comment: "Invalid Password"
)
case .notFound:
return NSLocalizedString(
"The specified item could not be found.",
comment: "Resource Not Found"
)
case .unexpected(_):
return NSLocalizedString(
"An unexpected error occurred.",
comment: "Unexpected Error"
)
}
}
}
Throw Custom Errors
Functions marked as throws
in Swift can throw custom errors directly:
func isAvailable(resourcePath: String) throws {
if FileManager.default.fileExists(atPath: path) {
return true
}
else {
throw CustomError.notFound
}
}
Catch Custom Errors
Custom errors can be individually caught and handled using the do
catch
syntax. Use catch
followed by the specific error to catch the error and apply specific handling logic:
func open(resourcePath: String) {
do {
try isAvailable(resourcePath: resourcePath)
// Handle opening the resource
}
catch CustomError.notFound {
// Handle custom error
}
catch {
// Handle other errors
}
}
Create Your Own Errors In Swift
That’s it! By conforming to Error
, CustomStringConvertible
, and LocalizedError
you can implement descriptive, clear, and actionable custom errors in Swift.
Ideally, errors should never occur. File we need should always be available and networks should always be present and reliable. Unfortunately this reality is not ideal and we have to deal with the consequences. Thankfully the Swift team included a great way to handle these deviations from the ideal in Swift 2. Swift Error Handling lets us quickly and easily warn the compiler whether a function can throw an error, actually throw it, and handle it in whatever way seems appropriate.
There is one major difference between how Objective-C handles errors, and the new Swift Error Handling. If you done any work with NSError in Objective-C, be honest, have you ever given “nil” instead of an NSError pointer when you dealt with a method that could throw an error? Yeah, I thought so. In Objective-C, to PROPERLY deal with errors, you would have to do something like this:
NSError *err = nil; NSString *fileStuff = [[NSString alloc] initWithContentsOfFile:@"someFile.txt" encoding:NSUTF8StringEncoding error:&err];
What this does is create an empty NSError object, which you then pass into that initializer with the ampersand ( & ) character, which is an operator to tell it to pass its memory address. Then, if there is an error, the called function or initializer would then find the spot in memory referred to by that address, place the appropriate NSError in it, and return from the function. This was a way to have multiple values returned from a function, by leaving an address where the new value could be placed. It was carried over to Swift as the “inout” keyword in a function prototype.
Now, if there was an error, you could look into that “err” variable and see which one it was and deal with it appropriately. But, if you didn’t care about the errors, you could just pass in the value “nil” directly, instead of the error pointer (the &err), and then any NSError thrown was just dropped into the oblivion that is nil.
In Swift with its new error handling syntax, dealing with errors is very explicit. Functions make it really obvious that they can throw an error, and your program must acknowledge such, and deal with it appropriately (or very explicitly ignore it).
Creating a Swift Error
How’s that for a weird title, actually creating an error? Well, actually we are creating an entity that will represent what error actually occurred in the program. Let’s say this app needs to read from a file, but there are a few different things that could happen when it tries. Everything could work fine, of course, or the file might not even exist. If it exists, the user might not have sufficient permissions to read it, or the file could have been damaged or corrupted somehow. For a function that will read this file, let’s create a Swift Error to represent those options:
enum FileReadingError: Error { case FileNotFound case InsufficientPermission case FileCorrupted }
The easiest way to create a Swift Error is to create an enumeration that conforms to the ErrorType protocol, as shown above. Then, you make cases to represent the different error conditions. Technically anything can conform to the ErrorType protocol, so you COULD use a struct or a class, but I personally can’t think of a very good reason to. The enum cases are a PERFECT way to represent a limited number of possible errors by name. They are made even better with the ability to have associated values, such as having the InsufficientPermission showing what permission level the current user is. If you want to read more about enumerations, check out the post Enumerations in Swift.
Now, let’s create a function that can throw this error:
func pretendToReadTestFile() throws -> Data { throw FileReadingError.FileNotFound }
Okay, this doesn’t ACTUALLY return anything and automatically throws the error, but we’re just looking at the mechanics of how to do it, not actually write a function to read a file and return its data.
Firstly, you have to mark the method as able to throw an error. That is simply done by using the “throws” keyword after the arguments, but before the arrow ” -> ” used to denote the return type. Then, inside the function, to throw the error, you simply type “throw” and the enumeration case that you want to send as the error. That’s it!
To actually call a function that throws, all you need to do is type the keyword “try” before the function call:
let data = try pretendToReadTestFile()
Handling Swift Errors
There are 4 main ways to handle Swift Errors:
Make Someone Else Do It
The first way is to not handle it at all and make someone else do it. For this one, you mark that your method that calls the throwing function itself is a throwing function. Then, whatever calls this new function will have to handle it. Eventually SOMETHING has to handle the error properly, but not necessarily that which calls the throwing function. For instance, if we had a File Management object that handles multiple aspects of file management like reading and writing, we might just want to punt that error up to whoever called the file manager to instead of handling it there.
To do this, just mark the calling function with the “throws” keyword. You still have to mark the function call to the actual function that might throw an error with try. If you want to store the return from a throwing function, you just call the function and store its data like normal, but you place the “try” between the equals sign and the function call.
func getDataFromSomething() throws -> Data { let data = try pretendToReadTestFile() return data }
Handle Specific Swift Errors
This way will probably look the most familiar to those that use exception handling in other languages. Swift Error handling is significantly different from exception handling though, and much more faster. Throwing a Swift Error is more like an alternative return statement, at least as far as how we use it: Instead of returning the intended value from a function call, it returns the appropriate Swift Error.
Basically you wrap the call to the throwing function in a “do” statement. Then after that you make “catch” statements, kind of like a Switch’s “case” statement, or an else-if for an if-statement, for the Swift Errors you are interested in, like so:
do { let someData = try pretendToReadTestFile() } catch FileReadingError.FileNotFound { print("The file was not found.") }
All the “do” block does is contain the code that calls the throwing function and diverts it to the appropriate catch statement if an error is thrown, VERY much like a switch statement and its cases. Also, you might not know of all of the possible errors that can be thrown, so we also have something equivalent to the “default” case in the Switch statement, or the “else” in an if-statement, which is just “catch” without a specific Swift Error mentioned:
do { let someData = try pretendToReadTestFile() } catch { print("Something weird happened.") }
Make throwing function’s return Optional
If you don’t care what the error is, but just need to know either the returned value, or to know that there IS no value to return, that sounds
like a job for Swift Optionals. This way, even if the return type isn’t optional, you basically tell the compiler “If there was an error, I don’t care what it is, just set the value to nil”. You call this with a question mark after the “try” keyword like this:
let possibleData = try? pretendToReadTestFile() //possibleData now contains "nil"
It is up to you when to handle specific errors with the do-catch statement or to use this style. If you really don’t need the reason, or the reason is rather obvious, using “try?” might just be the right tool. If you are making a network call, does it really matter to the user of your app if there was an error for a bad URL, a lost connection, or the host wasn’t found? It might, but for some apps it might not, all 3 of those mean you don’t have data you were trying to get, and “nil” may suffice to tell your code what to do since it doesn’t have the data it was requesting.
Assure the compiler it won’t throw an Error
If there’s a version with a question mark, you can bet there’s a version with an exclamation point. If you use “try!”, much like forced unwrapping of an optional, you call the throwing function, and if it DOES throw, your app will crash. If you are POSITIVE the call won’t throw, you can use this one. Apple’s iBook suggests one reason may be when using a throwing function to read a file contained in your apps bundle. Since it comes with the app, it will always be there, so you should never see a “File Not Found” or similar error. If you do, well, there are probably bigger problems to worry about.
In the case of our function that always throws an error, using this will cause a crash of course, but for an example of how to use it (and to see what the crash looks like):
let definitelyNoData = try! pretendToReadTestFile() //error: 'try!' expression unexpectedly raised an error: FileReadingError.FileNotFound
Conclusion
I am quite happy with how the Swift Team implemented error handling in Swift. It is a rather familiar way to handle it on the surface, but under the hood it is much faster, and less destructive than its cousin “Exception Handling”. No jumping up the call stack, just a different return telling us the nature of an error.
I hope you found this article helpful. If you did, please don’t hesitate to share this post on Twitter or your social media of choice, every share helps. Of course, if you have any questions, don’t hesitate to contact me on the Contact Page, or on Twitter @CodingExplorer, and I’ll see what I can do. Thanks!
Sources
- The Swift Programming Language – Apple Inc.
Обработка ошибок
Обработка ошибок — это процесс реагирования на возникновение ошибок и восстановление после появления ошибок в программе. Swift предоставляет первоклассную поддержку при генерации, вылавливании и переносе ошибок, устранении ошибок во время выполнения программы.
Некоторые операции не всегда гарантируют полное выполнение или конечный результат. Опционалы используются для обозначения отсутствия значения, но когда случается сбой, важно понять, что вызвало сбой, для того, чтобы соответствующим образом изменить код.
В качестве примера, рассмотрим задачу считывания и обработки данных из файла на диске. Задача может провалиться по нескольким причинам, в том числе: файл не существует по указанному пути, или файл не имеет разрешение на чтение, или файл не закодирован в необходимом формате. Отличительные особенности этих различных ситуаций позволяют программе решать некоторые ошибки самостоятельно и сообщать пользователю какие ошибки она не может решить сама.
Отображение и генерация ошибок
В Swift ошибки отображаются значениями типов, которые соответствуют протоколу Error. Этот пустой протокол является индикатором того, что это перечисление может быть использовано для обработки ошибок.
Перечисления в Swift особенно хорошо подходят для группировки схожих между собой условий возникновения ошибок и соответствующих им значений, что позволяет получить дополнительную информацию о природе самой ошибки. Например, вот как отображаются условия ошибки работы торгового автомата внутри игры:
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
Генерация ошибки позволяет указать, что произошло что-то неожиданное и обычное выполнение программы не может продолжаться. Для того чтобы «сгенерировать» ошибку, вы используете инструкцию throw. Например, следующий код генерирует ошибку, указывая, что пять дополнительных монет нужны торговому автомату:
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
Обработка ошибок
Когда генерируется ошибка, то фрагмент кода, окружающий ошибку, должен быть ответственным за ее обработку: например, он должен исправить ее, или испробовать альтернативный подход, или просто информировать пользователя о неудачном исполнении кода.
В Swift существует четыре способа обработки ошибок. Вы можете передать (propagate) ошибку из функции в код, который вызывает саму эту функцию, обработать ошибку, используя инструкцию do-catch, обработать ошибку, как значение опционала, или можно поставить утверждение, что ошибка в данном случае исключена. Каждый вариант будет рассмотрен далее.
Когда функция генерирует ошибку, последовательность выполнения вашей программы меняется, поэтому важно сразу обнаружить место в коде, которое может генерировать ошибки. Для того, чтобы выяснить где именно это происходит, напишите ключевое слово try — или варианты try? или try!— до куска кода, вызывающего функцию, метод или инициализатор, который может генерировать ошибку. Эти ключевые слова описываются в следующем параграфе.
Заметка
Обработка ошибок в Swift напоминает обработку исключений (exceptions) в других языках, с использованием ключевых слов try, catch и throw. В отличие от обработки исключений во многих языках, в том числе и в Objective-C- обработка ошибок в Swift не включает разворачивание стека вызовов, то есть процесса, который может быть дорогим в вычислительном отношении. Таким образом, производительные характеристики инструкции throw сопоставимы с характеристиками оператора return.
Передача ошибки с помощью генерирующей функции
Чтобы указать, что функция, метод или инициализатор могут генерировать ошибку, вам нужно написать ключевое слово throws в реализации функции после ее параметров. Функция, отмеченная throws называется генерирующей функцией. Если у функции установлен возвращаемый тип, то вы пишете ключевое слово throws перед стрелкой возврата (->).
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
Генерирующая функция передает ошибки, которые возникают внутри нее в область вызова этой функции.
Заметка
Только генерирующая ошибку функция может передавать ошибки. Любые ошибки, сгенерированные внутри non-throwing функции, должны быть обработаны внутри самой функции.
В приведенном ниже примере VendingMachine класс имеет vend(itemNamed: ) метод, который генерирует соответствующую VendingMachineError, если запрошенный элемент недоступен, его нет в наличии, или имеет стоимость, превышающую текущий депозит:
struct Item {
var price: Int
var count: Int
}
class VendingMachine {
var inventory = [
"Candy Bar": Item(price: 12, count: 7),
"Chips": Item(price: 10, count: 4),
"Pretzels": Item(price: 7, count: 11)
]
var coinsDeposited = 0
func vend(itemNamed name: String) throws {
guard let item = inventory[name] else {
throw VendingMachineError.invalidSelection
}
guard item.count > 0 else {
throw VendingMachineError.outOfStock
}
guard item.price <= coinsDeposited else {
throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
}
coinsDeposited -= item.price
var newItem = item
newItem.count -= 1
inventory[name] = newItem
print("Dispensing (name)")
}
}
Реализация vend(itemNamed: ) метода использует оператор guard для раннего выхода из метода и генерации соответствующих ошибок, если какое-либо требование для приобретения закуски не будет выполнено. Потому что инструкция throw мгновенно изменяет управление программой, и выбранная позиция будет куплена, только если все эти требования будут выполнены.
Поскольку vend(itemNamed: ) метод передает все ошибки, которые он генерирует, вызывающему его коду, то они должны быть обработаны напрямую, используя оператор do-catch, try? или try!, или должны быть переданы дальше. Например, buyFavoriteSnack(person:vendingMachine: ) в примере ниже — это тоже генерирующая функция, и любые ошибки, которые генерирует метод vend(itemNamed: ), будут переноситься до точки, где будет вызываться функция buyFavoriteSnack(person:vendingMachine: ).
let favoriteSnacks = [
"Alice": "Chips",
"Bob": "Licorice",
"Eve": "Pretzels"
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
let snackName = favoriteSnacks[person] ?? "Candy Bar"
try vendingMachine.vend(itemNamed: snackName)
}
В этом примере, функция buyFavoriteSnack(person:vendingMachine: ) подбирает любимые закуски данного человека и пытается их купить, вызывая vend(itemNamed: ) метод. Поскольку метод vend(itemNamed: ) может сгенерировать ошибку, он вызывается с ключевым словом try перед ним.
Генерирующие ошибку инициализаторы могут распространять ошибки таким же образом, как генерирующие ошибку функции. Например, инициализатор структуры PurchasedSnack в списке ниже вызывает генерирующую ошибку функции как часть процесса инициализации, и он обрабатывает любые ошибки, с которыми сталкивается, путем распространения их до вызывающего его объекта.
struct PurchasedSnack {
let name: String
init(name: String, vendingMachine: VendingMachine) throws {
try vendingMachine.vend(itemNamed: name)
self.name = name
}
}
Обработка ошибок с использованием do-catch
Используйте инструкцию do-catch для обработки ошибок, запуская блок кода. Если выдается ошибка в коде условия do, она соотносится с условием catch для определения того, кто именно сможет обработать ошибку.
Вот общий вид условия do-catch:
do {
try выражение
выражение
} catch шаблон 1 {
выражение
} catch шаблон 2 where условие {
выражение
} catch шаблон 3, шаблон 4 where условие {
выражение
} catch {
выражение
}
Вы пишете шаблон после ключевого слова catch, чтобы указать какие ошибки могут обрабатываться данным пунктом этого обработчика. Если условие catch не имеет своего шаблона, то оно подходит под любые ошибки и связывает ошибки к локальной константе error. Более подробно о соответствии шаблону см. Шаблоны.
Например, следующий код обрабатывает все три случая в перечислении VendingMachineError, но все другие ошибки должны быть обработаны окружающей областью:
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
} catch VendingMachineError.invalidSelection {
print("Ошибка выбора.")
} catch VendingMachineError.outOfStock {
print("Нет в наличии.")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
print("Недостаточно средств. Пожалуйста вставьте еще (coinsNeeded) монетки.")
} catch {
print("Неожиданная ошибка: (error).")
}
// Выведет "Недостаточно средств. Пожалуйста вставьте еще 2 монетки.
В приведенном выше примере, buyFavoriteSnack(person:vendingMachine: ) функция вызывается в выражении try, потому что она может сгенерировать ошибку. Если генерируется ошибка, выполнение немедленно переносится в условия catch, которые принимают решение о продолжении передачи ошибки. Если ошибка не генерируется, остальные операторы do выполняются.
В условии catch не нужно обрабатывать все возможные ошибки, которые может вызвать код в условии do. Если ни одно из условий catch не обрабатывает ошибку, ошибка распространяется на окружающую область. Однако распространяемая ошибка должна обрабатываться некоторой внешней областью. В функции nonthrowing условие включения do-catch должно обрабатывать ошибку. В функции throwing либо включающая условие do-catch, либо вызывающая сторона должна обрабатывать ошибку. Если ошибка распространяется на область верхнего уровня без обработки, вы получите ошибку исполнения.
Например, приведенный ниже пример можно записать так, чтобы любая ошибка, которая не является VendingMachineError, вместо этого захватывалась вызывающей функцией:
func nourish(with item: String) throws {
do {
try vendingMachine.vend(itemNamed: item)
} catch is VendingMachineError {
print("Некорректный вывод, нет в наличии или недостаточно денег.")
}
}
do {
try nourish(with: "Beet-Flavored Chips")
} catch {
print("Unexpected non-vending-machine-related error: (error)")
}
// Выведет "Некорректный вывод, нет в наличии или недостаточно денег."
В nourish(with: ), если vend(itemNamed : ) выдает ошибку, которая является одним из кейсов перечисления VendingMachineError, nourish(with: ) обрабатывает ошибку, печатая сообщение. В противном случае, nourish(with: ) распространяет ошибку на свое место вызова. Ошибка затем попадает в общее условие catch.
Преобразование ошибок в опциональные значения
Вы можете использовать try? для обработки ошибки, преобразовав ее в опциональное значение. Если ошибка генерируется при условии try?, то значение выражения вычисляется как nil. Например, в следующем коде x и y имеют одинаковые значения и поведение:
func someThrowingFunction() throws -> Int {
// ...
}
let x = try? someThrowingFunction()
let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}
Если someThrowingFunction() генерирует ошибку, значение x и y равно nil. В противном случае значение x и y — это возвращаемое значение функции. Обратите внимание, что x и y являются опциональными, независимо от того какой тип возвращает функция someThrowingFunction().
Использование try? позволяет написать краткий код обработки ошибок, если вы хотите обрабатывать все ошибки таким же образом. Например, следующий код использует несколько попыток для извлечения данных или возвращает nil, если попытки неудачные.
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() { return data }
if let data = try? fetchDataFromServer() { return data }
return nil
}
Запрет на передачу ошибок
Иногда вы знаете, что функции throw или методы не сгенерируют ошибку во время исполнения. В этих случаях, вы можете написать try! перед выражением для запрета передачи ошибки и завернуть вызов в утверждение того, что ошибка точно не будет сгенерирована. Если ошибка на самом деле сгенерирована, вы получите сообщение об ошибке исполнения.
Например, следующий код использует loadImage(atPath: ) функцию, которая загружает ресурс изображения по заданному пути или генерирует ошибку, если изображение не может быть загружено. В этом случае, поскольку изображение идет вместе с приложением, сообщение об ошибке не будет сгенерировано во время выполнения, поэтому целесообразно отключить передачу ошибки.
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
Установка действий по очистке (Cleanup)
Вы используете оператор defer для выполнения набора инструкций перед тем как исполнение кода оставит текущий блок. Это позволяет сделать любую необходимую очистку, которая должна быть выполнена, независимо от того, как именно это произойдет — либо он покинет из-за сгенерированной ошибки или из-за оператора, такого как break или return. Например, вы можете использовать defer, чтобы удостовериться, что файл дескрипторов закрыт и выделенная память вручную освобождена.
Оператор defer откладывает выполнение, пока не происходит выход из текущей области. Этот оператор состоит из ключевого слова defer и выражений, которые должны быть выполнены позже. Отложенные выражения могут не содержать кода, изменяющего контроль исполнения изнутри наружу, при помощи таких операторов как break или return, или просто генерирующего ошибку. Отложенные действия выполняются в обратном порядке, как они указаны, то есть, код в первом операторе defer выполняется после кода второго, и так далее.
func processFile(filename: String) throws {
if exists(filename) {
let file = open(filename)
defer {
close(file)
}
while let line = try file.readline() {
// работаем с файлом.
}
// close(file) вызывается здесь, в конце зоны видимости.
}
}
Приведенный выше пример использует оператор defer, чтобы удостовериться, что функция open(_: ) имеет соответствующий вызов и для close(_: ).
Заметка
Вы можете использовать оператор defer, даже если не используете кода обработки ошибок.
Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.
# Error handling basics
Functions in Swift may return values, throw errors (opens new window), or both:
Any value which conforms to the ErrorType protocol (opens new window) (including NSError objects) can be thrown as an error. Enumerations (opens new window) provide a convenient way to define custom errors:
An error indicates a non-fatal failure during program execution, and is handled with the specialized control-flow constructs do
/catch
, throw
, and try
.
Errors can be caught with do
/catch
:
Any function which can throw an error must be called using try
, try?
, or try!
:
# Catching different error types
Let’s create our own error type for this example.
The Do-Catch syntax allows to catch a thrown error, and automatically creates a constant named error
available in the catch
block:
You can also declare a variable yourself:
It’s also possible to chain different catch
statements. This is convenient if several types of errors can be thrown in the Do block.
Here the Do-Catch will first attempt to cast the error as a CustomError
, then as an NSError
if the custom type was not matched.
In Swift 3, no need to explicitly downcast to NSError.
# Catch and Switch Pattern for Explicit Error Handling
In the client class:
# Disabling Error Propagation
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.
For example, the following code uses a loadImage(atPath:) function, which loads the image resource at a given path or throws an error if the image can’t be loaded. In this case, because the image is shipped with the application, no error will be thrown at runtime, so it is appropriate to disable error propagation.
# Create custom Error with localized description
Create enum
of custom errors
Create extension
of RegistrationError
to handle the Localized description.
Handle error:
For more information about errors, see The Swift Programming Language (opens new window).
Regardless of your skill, handling errors is a common task for any developer. As developers we have complete control of the code we write and features we include, but despite our best efforts, we don’t control everything. We don’t control the values our users enter or whether our applications have access to the resources they need. In this post we’re going to look at how we can handle these situations by looking at error handling in Swift.
Error Conditions
In Swift terminology an error condition is:
…a failed condition check that results in a portion of our code skipping its usual course of execution.”
As such error conditions generally fall into one of three camps:
- Logical Error Conditions
- Simple Error Conditions
- Recoverable Error Conditions
Let’s look at each of these in turn.
Logical Error Conditions
Logical error conditions are error conditions that are introduced as a result of the code we write. This category of error condition include things like accidentally calling a method on a variable we think is holding an object when the variable is in fact nil
or trying to access an element on a collection using an out-of-range subscript.
For the most part, the Swift language (with the help of the compiler), does its best to help us avoid these kinds of error condition but it is not perfect and when these types of error conditions do occur it usually leads to our apps crashing.
Simple Error Conditions
The next category of error conditions are the simple error conditions. Simple error conditions are errors that occur as a result of performing some kind of operation that can fail in some obvious way. The error conditions in this category are usually simple in nature and because of this they are usually easily understandable and normally don’t need much (if any), additional information to understand what went wrong.
Converting a String
into an Int
is one such example:
let value : Int? = Int("1") // 1
let value2 : Int? = Int("Hello") // nil
A String
value can either be converted to Int
or it can’t. There’s no real grey area and we don’t need any additional information to work out what went wrong.
In Swift we commonly handle simple error conditions using optional values – returning the desired value in the success case or nil
when an error condition is encountered.
Recoverable Error Conditions
Our final category of error conditions are the recoverable error conditions. Recoverable error conditions are error conditions that result from operations that fail in more complex ways and are going to be the main focus of this article.
Take the example of accessing the contents of a file on disk:
func contentsOf(file filename: String) -> String? {
// ...
}
Although here the contentsOfFile(file:)
method has been defined to return an optional value (NSString?
), in reality there are a whole range of reasons that the function may have failed.
What if the file doesn’t exist? What if we don’t have permissions to access the file? What if the file is in an incompatible format?
Although the optional return value tells us whether an error condition was encountered, there just isn’t enough information to diagnose which error condition occurred.
What we need then is a mechanism that we can use to not only indicate that an error has occurred but one that also allow us to encapsulate additional information about what went wrong so we can react to the different error conditions.
It turns out that in Swift, there are a couple of ways we can do this but before we take a look at them let’s first take a look at how errors are actually represented in Swift.
The ErrorProtocol
Protocol
In Swift, an error is a value used to report that an error condition has occurred. Error values in Swift are represented using value types that conform to the ErrorProtocol
protocol (this was renamed from the ErrorType
protocol in Swift 2.x).
The ErrorProtocol
protocol is actually an empty protocol – it doesn’t require conforming types to implement any methods nor does it require them to have any particular properties all a type has to do is declare its conformance.
Now if you’ve done any sort of Cocoa programming before, it won’t surprise you to find out that the NSError
class from the Foundation Framework already conforms to the ErrorProtocol
protocol. However, Swift also allows us to go beyond this and define error types of our own.
To define an error type of our own in Swift, all we have to do is declare a new type that conforms to the ErrorProtocol
protocol. Although we can do this with any Swift type (such as structs or classes), most commonly we use enumeration types for this task. Enumerations types are actually well suited to this for a number of reasons.
First they can be used to group together a set of related error values.
Take our file access example. We can define an new enumeration type that conforms to the ErrorProtocol
protocol and groups together the different error conditions we may encounter when accessing the contents of a file:
enum FileError : ErrorProtocol {
case notFound
case permissionDenied
case unknownFormat
}
In this example the new error type is called FileError
. As required it conforms to the ErrorProtocol
protocol and has three potential values:
.notFound
for when the file doesn’t exist,.permissionDenied
when we don’t have permission to access the file.unknownFormat
for when the file has an unknown or incompatible format.
Secondly, we can also, should we need to, make use of enumeration cases associated values to provide additional information about the nature of any error should it occur.
Ok, so we know how to represent errors but how do we solve our earlier problem and not only indicate that an error has occurred but also communicate which error occurred? Well, one option is to encapsulate our new error type within a result type.
Using Result Types
In previous posts, We’ve talked about optional values and we’ve seen how they are, under the hood, just a generic enumeration type with two values:
enum Optional<A> {
case some(A)
case none
}
Result types are actually very similar. A result type is a generic enumeration type that also has two cases – a failure case with an associated value of a type conforming to the ErrorProtocol
protocol, and a success case with an associated value of the desired result type. In code it looks something like this:
enum ResultType {
case failure(ErrorProtocol)
case success(T)
}
Let’s apply this idea to our previous example.
Remember in our previous example we were returning an optional string (String?
) to indicate the success or failure of our file access operation:
func contentsOf(file filename: String) -> String? {
// ...
}
We can instead, re-define our function to return a ResultType
value that is parameterised over the String
type:
func contentsOf(file filename: String) -> ResultType {
//...
}
Given our definition of the ResultType
, any code now using the function can check the return value to see whether the function completed successfully or encountered an error. In addition, if an error did occur we can also determine exactly what the error was:
let filename = "source.txt"
let result = contentsOf(file: filename)
switch result {
case let .success(content):
print(content)
case let .failure(error):
switch error {
case FileError.notFound:
print("Unable to find file (filename).")
case FileError.permissionDenied:
print("You do not have permission to access the file (filename).")
case FileError.unknownFormat:
print("Unable to open file (filename) - incompatible format.")
default:
print("Unknown error")
}
}
Note: Notice here that we have to include the default
case in our error switch
statement. As with all switch
statements in Swift, the switch
statement that checks the error values must be exhaustive. We must therefore ensure that we handle all values of type ErrorProtocol
.
We can however, constrain things a little further.
If we tweak the ResultType
to take two generic parameters instead of one, we can provide a type for both the associated value of the success case and for the associated value of the failure case:
enum ResultType {
case failure(E)
case success(T)
}
If we then parameterise our generic type over our more constrained FileError
type as well as the String
type:
func contentsOf(file filename: String) -> ResultType {
//…
return Result.failure(.notFound)
}
We can remove the need for our default case as the compiler knows that the errors can only be one of the three values we defined in our FileError
type:
let filename = "source.txt"
let result = contentsOf(file: filename)
switch result {
case let .success(content):
print(content)
case let .failure(error):
switch error {
case .notFound:
print("Unable to find file (filename).")
case .permissionDenied:
print("You do not have permission to access the file (filename).")
case .unknownFormat:
print("Unable to open file (filename) - incompatible format.")
}
}
Now, result types are a useful tool to have in our repertoire but as of Swift 2.0 the Swift language has gained a new error handling mechanism of it’s own and one that is baked into the language.
Throwing Errors with throw
In Swift, when we want to indicate that an error condition has occurred we have two options. We can either define and return a result type as we have done above or we can throw an error using Swift’s in-built error handling mechanism.
Throwing an error is just like creating a result type, it allows us to create a value that represents the encountered error condition and potentially attach additional information to that error. It won’t surprise you to find out that we do this using a throw
statement.
The throw
statement consists of the throw
keyword followed by an expression whose value conforms to the ErrorProtocol
protocol. For example:
throw FileError.notFound
In Swift, throw
statements can occur in the body of any function or method or within the body of any closure expression. (To make things easier I’m just going to refer to these all as functions for now). When executed, throw
statements cause execution of the current program scope to immediately end and the thrown error value to be propagated to the enclosing scope.
Error Handling the 10,000ft View
Ok. So far so good. We know how to throw an error but what do we do when these error values are actually thrown?
Generally speaking errors are not what we want to encounter in our code and when errors do occur (and trust me, they will) it is up to us to write code to detect and handle those errors.
The act of handling an error in Swift is known as catching it. So we’ve got throwing to create and propagate an error and catching to detect and handle the error. Throw. Catch. Get it?
Anyway, exactly where and when we handle errors in our code is up to us but there is one golden rule – not handling errors – ignoring them and hoping they go away – is not an option. The Swift compiler is actually pretty strict on this topic and requires that we handle all errors at some point in our chain of function calls – so much so that it won’t compile our code if we don’t!
Ok, so we have to handle and catch errors when they’re thrown but how exactly do we do that?
In Swift, we have two main courses of action when it comes to catching and handling errors:
- Propagate the error to another function further up the call chain (a.k.a. make some other piece of code handle it).
- Handle the error within the scope of the current function.
Which approach we take is dependent on what makes most sense for the particular situation but in the next few sections, we’ll take a closer look at each so we can make a more informed decision about which to choose. Let’s start by looking at how to propagate errors.
Propagating Errors
Error Propagation with throws
In Swift, whether or not a function can propagate an error is baked into the functions signature.
By default, a function in Swift cannot propagate errors. This applies to all types of function whether they be global functions, methods or closures. It also means that we must, by default, write code to catch any errors that may occur and handle them within the body of a function in which they occur.
Note: In the following sections I again refer to functions but this equally applies to methods and closures as well.
Despite these rules, sometimes it’s just not possible to handle errors within the scope of the current function. Maybe we don’t have enough information to handle it at that level or maybe it just doesn’t makes sense to do so. Whatever the reason, there are occasions when we want to propagate errors that have occurred within a function back to the code that called that function.
To indicate that a function may propagate an error back to it’s calling scope, Swift requires that we annotate such functions with the throws
(or as we’ll see shortly – rethrows
) keyword. Functions that are annotated in this manner are unsurprisingly known as throwing functions.
To annotate a function with the throws
(or rethrows
) keyword we write the keyword after the parameters in the functions declaration and, if the function returns a value, before the return arrow (->
). For example, we could re-write our fileContents(filename:)
example as follows:
func contentsOf(file filename: String) throws -> String {
//...
}
Calling Throwing Functions
In the previous section we looked at annotating a function with the throws
(or as we’ll see later the rethrows
) keyword in order to propagate errors from the function. The thing is, annotating the function in this manner also has a second purpose – it makes it crystal clear for anybody reading the signature of the function that that function may propagate an error back to it’s calling code.
However there is something missing. If we look at it from a different perspective, the perspective of the code that called the function, things are a little less clear:
let content = contentsOf(file: "test.txt")
Here we not really sure whether the function will propagate an error or not. This lack of clarity was recognised as an issue within the language and to make things clear from the perspective of the calling code, the Swift language also mandates that when we call a function that can propagate an error, we have to prefix that function call with the try
keyword (or as we’ll see the try?
or try!
keywords). Here’s an example:
let content = try contentsOfFile(file: "test.txt")
The inclusion of the try
keyword does two things. First it signals to the compiler that we know we are calling a function that may propagate an error and secondly (and probably more importantly), it makes it clear to anyone reading our code that the function being called may throw an error.
Ok, let’s look at a more advanced example.
Throwing Functions and Inheritance
Note: This section is a little more advanced and may be challenging if you haven’t got any previous experience of object-oriented design principles. Don’t feel disheartened by this though. Every great developers was at that point at one point or other. Just skip this section for now and come back to it later – I won’t be offended 😉
Still here? Good. Ok super-quick object-oriented design re-cap.
As you probably know, inheritance is an object-oriented concept where functionality in one class can inherit the behaviour and properties of a parent or super class. You’ll also probably know that as part of that inheritance mechanism, it is also possible for child classes to override the behaviour of the methods in the parent class.
Now, you might be wondering why I mention this, but it’s an important consideration when it comes to error handling in Swift.
Imagine the scenario. We have a parent class (Foo
) that has some method doSomething()
:
class Foo {
func doSomething() -> String {
return "Foo"
}
}
Notice that doSomething()
method is not annotated with the throws
keyword. As we’ve learnt this means that any errors that occur within the body of the doSomething()
method, must be handled within that method and any code calling the function is safe in the knowledge that no errors will be propagated.
Now imagine we are overriding that method in the child class (Bar
).
As we know, to override a method in the child class, the method in the child class must have the same signature as that of the parent class in order to override it. However, suppose we tried to annotate the method with the throws
keyword:
class Bar : Foo {
override func doSomething() throws -> String {
return "Bar"
}
}
The thing is, this code doesn’t compile.
In the context of error handling, Swift doesn’t allow us to override a non-throwing method in a parent class with a throwing method in a child class. The same rule applies when attempting to satisfy a protocol requirement for a non-throwing method with a method that actually throws.
If you think about it, in both cases it’s not that surprising.
Think about the code that might be calling the doSomething()
method on the parent (Foo
) class. None of the code is expecting the call to the method to result in an error being propagated, after all it’s not annotated with the throws
keyword and we’ve already talked about how only methods annotated with throws
can propagate errors. As we know though, due to the wonders of object-oriented polymorphism, an instance of a child class can be cast to an instance of a parent class. This makes things a little complicated.
Imagine if the child class Bar
was cast to an instance of the parent class Foo
and the doSomething()
method was called. Code calling that function would not expect the code to throw an error but the method of the underlying class (the child class Bar
) could actually throw – a real surprise to any of the calling code. The Swift language doesn’t like nasty surprises and to avoid this whole problem, overriding in this way is not allowed in Swift.
However, consider this scenario.
Imagine we had another class (Baz
) with a method that was already marked as throwing:
class Baz {
func doSomethingElse() throws -> String {
return "Foo"
}
}
Now imagine we had a child class (Qux
) that inherited from Baz
and overrode the doSomethingElse()
method:
class Qux : Baz {
override func doSomethingElse() -> String {
return "Bar"
}
}
Notice here that the doSomethingElse()
method is NOT marked as throwing in the child class. It might surprise you but this code does compile and is in fact perfectly legal.
If you think about it this again pretty logical. Consider the calling code again.
Any code that was calling the doSomethingElse()
method in the parent (Baz
) class already knows that there is a potential for the doSomethingElse()
method to propagate an error. That means that that code will have been written to the cases where either an error is propagated from the method call or no error occurs.
If, as in our previous scenario, the child class (Qux
) is then cast to an instance of the parent class (Baz
) and the doSomethingElse()
method is called, the fact that the doSomethingElse()
method doesn’t raise an error is already handled by within the calling code. No surprises and no problems.
If we abstract this away a little, the point is that in Swift, functions that cannot throw, are actually subsets of functions that can throw. This means that we can use a non-throwing function, anywhere we can use a throwing function but not the reverse.
Propagating Errors with rethrows
Now that we’re clear about the relationship between throwing functions and inheritance, let’s look at a slightly different scenario.
Imagine we had a simple logging function log
that takes a single parameter – a closure that returns the string to be logged:
func log(description:() -> String) -> Void {
print(description())
}
In this form, everything works fine. However, now imagine that the closure we supplied to the log
function could itself throw an error. What are the implications?
Well, if we called the closure within the body of the log
function it obviously might result in an error being thrown. We could modify the log
function to handle the error but equally we might want to propagate that error back to the calling function. This is where rethrows
comes in.
In Swift the rethrows
keyword is used to indicate that a function throws an error if and only if, one of its parameters throws an error. Functions or methods annotated in this manner are known as rethrowing functions or methods. Here’s an example:
func log(description: () throws -> String) rethrows -> () {
print(try description())
}
func nonThrowing() -> String {
return "Hello"
}
enum LogError : ErrorType {
case someError
}
func throwing() throws -> String {
// Do stuff....
throw LogError.someError
}
log(nonThrowing)
try log(throwing) // Has to be called with 'try' because it might throw
In the first call to the log()
function, the closure we supply as an argument (in this case the function nonThrowing()
) doesn’t throw an error and therefore, due to the rethrows
annotation, the log()
function also doesn’t throw an error. This means we don’t need to prefix the call with the try
keyword and are guaranteed that no error will be propagated.
Notice though that in the second call to the log()
function, we do have to include the try
keyword. In this case, because the closure we pass in as an argument throws an error, the log()
function in turn throws an error. As a result of this, we therefore have to include the try
keyword before the call to acknowledge that an error may be thrown.
Now, there are a few things to note with the rethrows
keyword.
Firstly, rethrowing functions MUST take at least one throwing function as an argument. The compiler will complain if the don’t:
func baz(completion: () -> String) rethrows -> String {
return completion()
}
// ERROR: 'rethrows' function must take a throwing function argument
Secondly, throwing functions can only propagate errors that are thrown by the functions they take as arguments or errors that propagate from enclosed scopes. This means that rethrowing functions can’t contain any throw
statements directly:
enum ExampleError : ErrorProtocol {
case someError
case someOtherError
}
func abc(completion: () -> String) rethrows -> String {
throw ExampleError.someError
}
// ERROR: a function declared 'rethrows' may only throw if its parameter does
They can however catch errors using a do-catch
statement (we’ll cover these shortly) and re-throw either the same or a different error which is then propagated to the enclosing scope:
func qux(completion: () throws -> String) rethrows -> String {
do {
return try completion()
} catch {
print("Inside Qux: (error)")
throw ExampleError.someOtherError
}
}
do {
print(try qux { () throws -> String in
throw ExampleError.someError
})
} catch {
print("Outside Qux: (error)")
}
Note: If you’re a bit confused about the do-catch
statement don’t worry, we’ll be looking at them in more detail shortly.
When it comes to rethrowing functions and inheritance, similar rules apply to those that we looked at earlier with throwing vs non-throwing functions.
Firstly, throwing methods can’t override rethrowing methods and throwing methods can’t satisfy protocol requirements for a rethrowing methods. This is for the same reasons as we looked at earlier. Code calling the throwing method knows whether the function will throw based on whether the functions supplied as an argument throw. If we overrode the function to always throw, the calling code might get some nasty surprises.
Conversely, rethrowing methods can override a throwing method and a rethrowing method can also satisfy a protocol requirement for a throwing method though. As we saw earlier, non-throwing methods are simply a subset of all throwing methods. This means that the calling code is already setup to handle any errors that may be thrown. If the method overridden method doesn’t throw a method then great, it doesn’t matter from the perspective of the calling code.
Handling Errors
Ok, let’s park error propagation for now and look at how we can actually handle errors rather than simply propagating the errors to an enclosing scope.
When it comes to handling errors we have three main options:
- Handle the error in a
do-catch
statement. - Handle the error as an optional value.
- Assert that the error will never occur.
As we’ve already touched on the do-catch
statement in the previous section let’s start with that.
Handling Errors with do-catch
Statement
In Swift, the do
statement is used to introduce a new scope. It is similar to the curly braces ({}
) in C and is used to delimit a new code block. The scope that the do
statement introduces is just like any other scope. This means that variables and constants declared within that scope can only be accessed within that scope and go out of scope when the do
statement ends. Now, you may be wondering why I mention the do
statement, after all this is an article on error handling. The things is, the do
statement has a trick up it’s sleeve. In addition to introducing a new scope, the do
statement can optionally contain one or more catch
clauses.
Each catch
clause contains a pattern that can be used to match against defined error conditions. The catch
clauses allows you to catch a thrown error value and handle that error.
From a high-level then, the do-catch
statement has the following general form:
do {
try expression
statements
}
}
catch pattern 1 {
statements
}
catch pattern 2 where condition {
statements
}
catch {
statements
}
We can use this in various forms to to catch different types of error should they occur:
enum InputError : ErrorProtocol {
case makeMissing
case mileageTooLow(Int)
case mileageTooHigh(Int)
}
func shouldBuyCar(make: String, mileage: Int) throws {
guard make.characters.count > 0 else {
throw InputError.makeMissing
}
switch mileage {
case mileage where mileage < 10:
throw InputError.mileageTooLow(mileage)
case mileage where mileage > 100:
throw InputError.mileageTooHigh(mileage)
default:
print("Buy it!")
}
}
do {
try shouldBuyCar(make: "Honda", mileage:120)
}
catch InputError.makeMissing {
print("Missing make")
}
catch let InputError.mileageTooHigh(x) where x > 150 {
print("Mileage way way too high...")
}
catch let InputError.mileageTooHigh(x) {
print("Mileage too high")
}
catch {
print("(error)")
}
// Mileage too high
It’s a bit of a long example so let’s walk through it.
So, the scenario is that we’re buying a car and we have a little function that takes in some details and decides whether to buy the car or not. (Not the best way of doing it I’m sure but it’ll do for our example).
At the top, we then have the shouldBuyCar(make:mileage:)
function. This function takes in the different parameters (the make and mileage) and decides whether we should buy the car. In this case the shouldBuyCar(make:mileage:)
function is marked as throwing as it can potentially throw one or three errors.
- If we supply an empty string for the cars make it will throw a
.makeMissing
error. - If the mileage is too low it will throw a
.mileageToLow
error and will attach the offending mileage as an associated value. - Finally if the mileage is too high it’ll throw a
.mileageToHigh
error again attaching the mileage that caused the issue.
In all other cases, it simply prints out the Buy it!
statement to the console.
Now, the meat of what we want to look at is actually at the bottom of the example.
First, we have our do
statement and within the body of the statement we make our call to the shouldBuyCar(make:mileage:)
function prefixing it with the try
keyword due to the fact that the function call may result in an error being thrown.
After that, we have a number of catch
clauses. Each of these catch
clauses contains a different pattern that will match against any error that is propagated from the call to the shouldBuyCar(make:mileage:)
function.
First we use the identifier pattern to check for an explicit error value. This will match any and all makeMissing
errors e.g.:
catch Car.InputError.makeMissing {
// Handle the error.
print("Missing make")
}
We also use an enumeration case pattern to match the .mileageTooHigh
error value and extract any associated mileage:
catch InputError.mileageTooHigh(x) {
print("Mileage too high")
}
We can also combine that with a where
clause to further constrain things as we’ve done in the second catch clause:
catch let InputError.mileageTooHigh(x) where x > 150 {
print("Mileage way way too high...")
}
Note: The catch
clauses are evaluated sequentially, much like in a switch
statement and will execute the first catch
clause that matches. In this case we have to include our more contained catch
clause containing the where
clause before it’s more generic sibling if it is to ever match.
One other more subtle point to be aware of is that as with switch
statements, the Swift compiler also attempts to infer whether the catch
clauses in a do-catch
statement are exhaustive. If a determination can be made, the compiler views the error has being handled. If however the catch
clauses are not exhaustive, the error automatically propagates out of the containing scope, either to an enclosing catch clause or out of the function. As you might expect, if the error is propagated out of the function, the function must then be marked with the throws
keyword to alert calling code of that possibility.
So that leaves the question of how to ensure that we have handled all potential errors.
We actually have a number of options for achieving this. First we can use a catch clause with a pattern that matches all errors, such as a wildcard pattern (_
). The issue with this approach though is that we don’t get access the error value itself it is simply swallowed.
As an alternative, we can however use the catch
clause on it’s own as I’ve done in the last catch
clause in this example. When used in this manner the catch
clause automatically matches and binds any error that it encounters to a local constant named error
. This error
value can then be accessed within the body of that catch
clause.
Converting Errors Into Optional Values with try?
In addition to the do-catch
statement, Swift also provides us with a couple of other options when it comes to handling errors. The first of these is the try?
statement. We’ve already seen it’s sibling the try
statement earlier in this article.
As you know, the try
statement is used in front of calls to functions, methods or closures that may throw an error. try?
is used in the same place however it’s behaviour is a little different. With the try?
statement, if an error is thrown whilst evaluating the associated expression (such in a call to a throwing function) the result of the expression is set to nil
rather than propagating the error back to the enclosing scope. Essentially this allows us to handle the error and convert it into an optional value in one move:
func throwingFunction() throws -> String {
// ...
}
let result = try? throwingFunction()
// Result is an String?
As with other techniques we’ve seen though, this approach does have it’s shortcomings the main one being that you don’t get any information about the exact error that occurred. However, in certain situations, this may be just what you want so it’s worth knowing about.
Preventing Error Propagation with try!
Our final option when it comes to error handling is the try!
statement. This is the more assertive version of it’s siblings.
Sometimes we are writing our code we know that a function won’t in fact throw an error at runtime despite the function being marked as such.
Maybe we have pre-validated the input data, maybe the circumstances are such that the error simply won’t be thrown. Whatever the reason, in these occasions we can use the try!
keyword before a potentially throwing expression to disable the propagation of errors and wrap the call in a runtime assertion that no error will be thrown.
For example, say we had a function that threw an error if it was passed a negative value (and yes, I know we could define the parameter as UInt
but go with it). We might choose to use the try!
statement if we had already pre-validated that the argument we were passing to the function was definitely positive:
enum ValueError : ErrorProtocol {
case negativeValue
}
func squarePositive(value: Int) throws -> Int {
guard value >= 0 else {
throw ValueError.negativeValue
}
return value * value
}
let output : Int
let input = 10
if input >= 0 {
output = try! squarePositive(value: input)
print(output)
}
A contrived example I know, but you get the idea. The only thing to mention is that you need to be a little careful with the try!
statement. As I mentioned, the try!
statement wraps the call to the throwing function in a run-time assertion. This means you need to be absolutely sure that no error will be thrown otherwise you risk your code crashing at runtime.
Error Propagation With Asynchronous Closures
The final thing I want to look at today is error handling and asynchronous operations. It’s another more advanced topic so skip this if you’re just getting started. Also, I’m not going to go into this in huge depth. This article is already pretty long but I wanted to mention it so that you had some idea about the possibilities. Anyway, let’s dip our toe in.
Asynchronous operations, as the name suggests, are where there is a delay between the initial call to a function and the response coming back. In Swift, this is commonly implemented through the use of completion handlers:
func asyncOperation(completion: String -> ()) -> Void {
DispatchQueue.global(attributes: .qosDefault).async {
// 1. Do stuff...
let result = "Hello world"
DispatchQueue.main.async {
completion(result)
}
}
}
func completionHandler(result: String) {
print(result)
}
asyncOperation(completion: completionHandler)
// Hello world
Note: If you’re trying the above example in a Playground you’ll also have to add the following at the top of your playground page to ensure the playground provides enough time for the asyncOperation(completion:)
function to complete:
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
Now, think about error handling and this example above. What if an error occurred within the body of the asynchronous operation? How could we handle it?
Well, with everything we’ve covered up until now, you should be armed with most of the tools you’d need. One option is to simply handle the error within the body of the asyncOperation(completion:)
function by using a do-catch
statement. Notice here that I also modify the completion handler to take an optional value which will be set to nil
in the case of an error or the result otherwise:
enum ExampleError: ErrorProtocol {
case error
}
func throwingOperation() throws -> String {
throw ExampleError.error
}
func asyncOperation(operation:() throws -> String,
completion:String? -> ()) -> Void {
DispatchQueue.global(attributes: .qosDefault).async {
// Do stuff..
var result: String? = nil
do {
result = try operation()
} catch {
print(error)
}
DispatchQueue.main.async {
completion(result)
}
}
}
func completionHandler(result: String?) {
print(result)
}
asyncOperation(operation: throwingOperation, completion: completionHandler)
Now this is fine, but what if we wanted to return any error that occurred back to the calling scope?
Well another option is to go back to the start of this article, and make use of the ResultType
we talked about:
enum ExampleError: ErrorProtocol {
case someError
}
enum ResultType<T> {
case err(ErrorProtocol)
case success(T)
}
func throwingOperation() throws -> String {
throw ExampleError.someError
}
func asyncOperation(operation:() throws -> String,
completion:(ResultType<String>) -> ()) -> Void {
DispatchQueue.global(attributes: .qosDefault).async {
// Do stuff..
var result: ResultType<String>
do {
let output = try operation()
result = .success(output)
} catch {
result = .err(error)
}
DispatchQueue.main.async {
completion(result)
}
}
}
func completionHandler(result: ResultType<String>) {
switch result {
case let .success(value):
print(value)
case let .err(error):
print(error)
}
}
asyncOperation(operation: throwingOperation, completion: completionHandler)
Another option, and one that is possible but feels like a bit of a hack is to us an inner closure:
enum ExampleError: ErrorProtocol {
case someError
}
enum ResultType<T> {
case err(ErrorProtocol)
case success(T)
}
func throwingOperation() throws -> String {
throw ExampleError.someError
}
func asyncOperation(operation:() throws -> String,
completion: (innerclosure: () throws -> String) -> Void) -> Void {
DispatchQueue.global(attributes: .qosDefault).async {
// Do stuff..
var result: () throws -> String
do {
let output = try operation()
result = { output }
} catch {
result = { throw error }
}
DispatchQueue.main.async {
completion(innerclosure: result)
}
}
}
func completionHandler(result: () throws -> String) {
do {
let value = try result()
print(value)
} catch {
print(error)
}
}
asyncOperation(operation: throwingOperation, completion: completionHandler)
Here, instead of returning a ResultType
we return a closure that encapsulates the result of the async operation. In the case of success, the returned closure will return the result. In case of error, the returned closure will (re?)throw the original error value. The calling code can then extract this result using Swift’s normal do-catch
handling. The main downside with this inner-closure approach is that it doesn’t work with methods you don’t own. You’ll notice that in order to get this to work, the signature of the completion handler needs to be modified to accept a closure rather than a normal return value. This is works well if you own that code, but if you’re using one of the standard library functions then not so much. Despite this I thought it was worth mentioning as it’s good to have this encapsulated closure approach as an option.
Wrapping Up
Ok, that pretty much wraps it up for error handling in Swift.
We’ve looked at the different types of error that can occur, seen how to throw errors of our own and then spent the latter part of this article looking at how we can handle those errors when they do occur.
As we’ve seen, error handling took two major forms, either propagating errors or handling them using either the do-catch
, try?
or try!
statements. Whatever your approach, hopefully this article will have given you a much better understanding of error handling in Swift and will allow you to start leveraging it’s power within your own code.