Error handling swift

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.

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:

  1. enum VendingMachineError: Error {
  2. case invalidSelection
  3. case insufficientFunds(coinsNeeded: Int)
  4. case outOfStock
  5. }

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:

  1. 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 docatch 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 (->).

  1. func canThrowErrors() throws -> String
  2. 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:

  1. struct Item {
  2. var price: Int
  3. var count: Int
  4. }
  5. class VendingMachine {
  6. var inventory = [
  7. «Candy Bar»: Item(price: 12, count: 7),
  8. «Chips»: Item(price: 10, count: 4),
  9. «Pretzels»: Item(price: 7, count: 11)
  10. ]
  11. var coinsDeposited = 0
  12. func vend(itemNamed name: String) throws {
  13. guard let item = inventory[name] else {
  14. throw VendingMachineError.invalidSelection
  15. }
  16. guard item.count > 0 else {
  17. throw VendingMachineError.outOfStock
  18. }
  19. guard item.price <= coinsDeposited else {
  20. throw VendingMachineError.insufficientFunds(coinsNeeded: item.pricecoinsDeposited)
  21. }
  22. coinsDeposited -= item.price
  23. var newItem = item
  24. newItem.count -= 1
  25. inventory[name] = newItem
  26. print(«Dispensing (name)«)
  27. }
  28. }

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 docatch 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.

  1. let favoriteSnacks = [
  2. «Alice»: «Chips»,
  3. «Bob»: «Licorice»,
  4. «Eve»: «Pretzels»,
  5. ]
  6. func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
  7. let snackName = favoriteSnacks[person] ?? «Candy Bar»
  8. try vendingMachine.vend(itemNamed: snackName)
  9. }

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.

  1. struct PurchasedSnack {
  2. let name: String
  3. init(name: String, vendingMachine: VendingMachine) throws {
  4. try vendingMachine.vend(itemNamed: name)
  5. self.name = name
  6. }
  7. }

Handling Errors Using Do-Catch¶

You use a docatch 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 docatch statement:

  1. do {
  2. try expression
  3. statements
  4. } catch pattern 1 {
  5. statements
  6. } catch pattern 2 where condition {
  7. statements
  8. } catch pattern 3, pattern 4 where condition {
  9. statements
  10. } catch {
  11. statements
  12. }

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.

  1. var vendingMachine = VendingMachine()
  2. vendingMachine.coinsDeposited = 8
  3. do {
  4. try buyFavoriteSnack(person: «Alice», vendingMachine: vendingMachine)
  5. print(«Success! Yum.»)
  6. } catch VendingMachineError.invalidSelection {
  7. print(«Invalid Selection.»)
  8. } catch VendingMachineError.outOfStock {
  9. print(«Out of Stock.»)
  10. } catch VendingMachineError.insufficientFunds(let coinsNeeded) {
  11. print(«Insufficient funds. Please insert an additional (coinsNeeded) coins.»)
  12. } catch {
  13. print(«Unexpected error: (error))
  14. }
  15. // 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 docatch statement must handle the error. In a throwing function, either an enclosing docatch 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:

  1. func nourish(with item: String) throws {
  2. do {
  3. try vendingMachine.vend(itemNamed: item)
  4. } catch is VendingMachineError {
  5. print(«Couldn’t buy that from the vending machine.»)
  6. }
  7. }
  8. do {
  9. try nourish(with: «Beet-Flavored Chips»)
  10. } catch {
  11. print(«Unexpected non-vending-machine-related error: (error)«)
  12. }
  13. // 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:

  1. func eat(item: String) throws {
  2. do {
  3. try vendingMachine.vend(itemNamed: item)
  4. } catch VendingMachineError.invalidSelection, VendingMachineError.insufficientFunds, VendingMachineError.outOfStock {
  5. print(«Invalid selection, out of stock, or not enough money.»)
  6. }
  7. }

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:

  1. func someThrowingFunction() throws -> Int {
  2. // …
  3. }
  4. let x = try? someThrowingFunction()
  5. let y: Int?
  6. do {
  7. y = try someThrowingFunction()
  8. } catch {
  9. y = nil
  10. }

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.

  1. func fetchData() -> Data? {
  2. if let data = try? fetchDataFromDisk() { return data }
  3. if let data = try? fetchDataFromServer() { return data }
  4. return nil
  5. }

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.

  1. 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.

  1. func processFile(filename: String) throws {
  2. if exists(filename) {
  3. let file = open(filename)
  4. defer {
  5. close(file)
  6. }
  7. while let line = try file.readline() {
  8. // Work with the file.
  9. }
  10. // close(file) is called here, at the end of the scope.
  11. }
  12. }

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.

Обработка ошибок

Обработка ошибок — это процесс реагирования на возникновение ошибок и восстановление после появления ошибок в программе. 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.

Время прочтения
5 мин

Просмотры 9.7K

Сегодня мы приготовили перевод для тех, кто так же, как автор статьи, при изучении Документации языка программирования Swift избегает главы «Error Handling».

Из статьи вы узнаете:

  • что такое оператор if-else и что с ним не так;
  • как подружиться с Error Handling;
  • когда стоит использовать Try! и Try?

Моя история

Когда я был младше, я начинал изучать документацию языка Swift. Я по несколько раз прочёл все главы, кроме одной: «Error Handling». Отчего-то мне казалось, что нужно быть профессиональным программистом, чтобы понять эту главу.

Я боялся обработки ошибок. Такие слова, как catch, try, throw и throws, казались бессмысленными. Они просто пугали. Неужели они не выглядят устрашающими для человека, который видит их в первый раз? Но не волнуйтесь, друзья мои. Я здесь, чтобы помочь вам.

Как я объяснил своей тринадцатилетней сестре, обработка ошибок – это всего лишь ещё один способ написать блок if-else для отправки сообщения об ошибке.

Сообщение об ошибке от Tesla Motors

Как вы наверняка знаете, у автомобилей Tesla есть функция автопилота. Но, если в работе машины по какой-либо причине происходит сбой, она просит вас взять руль в руки и сообщает об ошибке. В этом уроке мы узнаем, как выводить такое сообщение с помощью Error Handling.

Мы создадим программу, которая будет распознавать такие объекты, как светофоры на улицах. Для этого нужно знать как минимум машинное обучение, векторное исчисление, линейную алгебру, теорию вероятности и дискретную математику. Шутка.

Знакомство с оператором if-else

Чтобы максимально оценить Error Handling в Swift, давайте оглянемся в прошлое. Вот что многие, если не все, начинающие разработчики сделали бы, столкнувшись с сообщением об ошибке:

var isInControl = true

func selfDrive() {
 if isInControl {
  print("You good, let me ride this car for ya")
 } else {
  print("Hold the handlebar RIGHT NOW, or you gone die")
 }
}

selfDrive() // "You good..."

Проблема

Самая большая проблема заключается в удобочитаемости кода, когда блок else становится слишком громоздким. Во-первых, вы не поймёте, содержит ли сама функция сообщение об ошибке, до тех пор, пока не прочитаете функцию от начала до конца или если не назовёте ее, например, selfDriveCanCauseError, что тоже сработает.

Смотрите, функция может убить водителя. Необходимо в недвусмысленных выражениях предупредить свою команду о том, что эта функция опасна и даже может быть смертельной, если невнимательно с ней обращаться.

С другой проблемой можно столкнуться при выполнении некоторых сложных функций или действий внутри блока else. Например:

else {
 print("Hold the handle bar Right now...")

 // If handle not held within 5 seconds, car will shut down 
 // Slow down the car
 // More code ...
 // More code ...

}

Блок else раздувается, и работать с ним – все равно что пытаться играть в баскетбол в зимней одежде (по правде говоря, я так и делаю, так как в Корее достаточно холодно). Вы понимаете, о чём я? Это некрасиво и нечитабельно.

Поэтому вы просто могли бы добавить функцию в блок else вместо прямых вызовов.


else { 
 slowDownTheCar()
 shutDownTheEngine()
}

Однако при этом сохраняется первая из выделенных мной проблем, плюс нет какого-то определённого способа обозначить, что функция selfDrive() опасна и что с ней нужно обращаться с осторожностью. Поэтому предлагаю погрузиться в Error Handling, чтобы писать модульные и точные сообщения об ошибках.

Знакомство с Error Handling

К настоящему времени вы уже знаете о проблеме If-else с сообщениями об ошибках. Пример выше был слишком простым. Давайте предположим, что есть два сообщения об ошибке:

  1. вы заблудились
  2. аккумулятор автомобиля разряжается.

Я собираюсь создать enum, который соответствует протоколу Error.

enum TeslaError: Error {
 case lostGPS
 case lowBattery 
}

Честно говоря, я точно не знаю, что делает Error протокол, но при обработке ошибок без этого не обойдешься. Это как: «Почему ноутбук включается, когда нажимаешь на кнопку? Почему экран телефона можно разблокировать, проведя по нему пальцем?»

Разработчики Swift так решили, и я не хочу задаваться вопросом об их мотивах. Я просто использую то, что они для нас сделали. Конечно, если вы хотите разобраться подробнее, вы можете загрузить программный код Swift и проанализировать его самостоятельно – то есть, по нашей аналогии, разобрать ноутбук или iPhone. Я же просто пропущу этот шаг.

Если вы запутались, потерпите еще несколько абзацев. Вы увидите, как все станет ясно, когда TeslaError превратится в функцию.

Давайте сперва отправим сообщение об ошибке без использования Error Handling.

var lostGPS: Bool = true
var lowBattery: Bool = false

func autoDriveTesla() {
 if lostGPS {
  print("I'm lost, bruh. Hold me tight")
  // A lot more code

 }
 if lowBattery {
  print("HURRY! ")
  // Loads of code 
 }
}

Итак, если бы я запустил это:


autoDriveTesla() // "HURRY! " 

Но давайте используем Error Handling. В первую очередь вы должны явно указать, что функция опасна и может выдавать ошибки. Мы добавим к функции ключевое слово throws.


func autoDriveTesla() throws { ... }

Теперь функция автоматически говорит вашим товарищам по команде, что autoDriveTesla – особый случай, и им не нужно читать весь блок.

Звучит неплохо? Отлично, теперь пришло время выдавать эти ошибки, когда водитель сталкивается с lostGPA или lowBattery внутри блока Else-If. Помните про enum TeslaError?

func autoDriveTesla() throws {
 if lostGPS {
  throw TeslaError.lostGPS

}
 if lowBattery {
  throw TeslaError.lowBattery
}

Я вас всех поймаю

Если lostGPS равно true, то функция отправит TeslaError.lostGPS. Но что делать потом? Куда мы будем вставлять это сообщение об ошибке и добавлять код для блока else?

print("Bruh, I'm lost. Hold me tight")

Окей, я не хочу заваливать вас информацией, поэтому давайте начнём с того, как выполнить функцию, когда в ней есть ключевое слово throws.

Так как это особый случай, вам необходимо добавлять try внутрь блока do при работе с этой функцией. Вы такие: «Что?». Просто последите за ходом моих мыслей ещё чуть-чуть.

do {
 try autoDriveTesla() 
}

Я знаю, что вы сейчас думаете: «Я очень хочу вывести на экран моё сообщение об ошибке, иначе водитель умрёт».

Итак, куда мы вставим это сообщение об ошибке? Мы знаем, что функция способна отправлять 2 возможных сообщения об ошибке:

  1. TeslaError.lowBattery
  2. TeslaError.lostGPS.

Когда функция выдаёт ошибку, вам необходимо её “поймать” и, как только вы это сделаете, вывести на экран соответствующее сообщение. Звучит немного запутанно, поэтому давайте посмотрим.

var lostGPS: Bool = false
var lowBattery: Bool = true

do {
 try autoDriveTesla() 
 } catch TeslaError.lostGPS {
  print("Bruh, I'm lost. Hold me tight")
 } catch TeslaError.lowBattery {
  print("HURRY! ")
 }
}

// Results: "HURRY! "

Теперь всё должно стать понятно. Если понятно не всё, вы всегда можете посмотреть моё видео на YouTube.

Обработка ошибок с Init

Обработка ошибок может применяться не только к функциям, но и тогда, когда вам нужно инициализировать объект. Допустим, если вы не задали имя курса, то нужно выдавать ошибку.

Если вы введёте tryUdemyCourse(name: «»), появится сообщение об ошибке.

Когда использовать Try! и Try?

Хорошо. Try используется только тогда, когда вы выполняете функцию/инициализацию внутри блока do-catch. Однако если у вас нет цели предупредить пользователя о том, что происходит, выводя сообщение об ошибке на экран, или как-то исправить ее, вам не нужен блок catch.

try?– что это?

Давайте начнём с try? Хотя это не рекомендуется,

let newCourse = try? UdemyCourse("Functional Programming")

try? всегда возвращает опциональный объект, поэтому необходимо извлечь newCourse

if let newCourse = newCourse { ... }

Если метод init выбрасывает ошибку, как, например

let myCourse = try? UdemyCourse("") // throw NameError.noName

то myCourse будет равен nil.

try! – что это?

В отличие от try? оно возвращает не опциональное значение, а обычное. Например,

let bobCourse = try! UdemyCourse("Practical POP")

bobCourse не опционально. Однако, если при методе инициализации выдается ошибка вроде,

let noCourseName = try! UdemyCourse("") // throw NameError.noName

то приложение упадёт. Так же как и в случае с принудительным извлечением с помощью !, никогда не используйте его, если вы не уверены на 101% в том, что происходит.

Ну вот и всё. Теперь вы вместе со мной поняли концепцию Error Handling. Легко и просто! И не нужно становиться профессиональным программистом.

Swift 2 & 3

Things have changed a bit in Swift 2, as there is a new error-handling mechanism, that is somewhat more similar to exceptions but different in detail.

1. Indicating error possibility

If function/method wants to indicate that it may throw an error, it should contain throws keyword like this

func summonDefaultDragon() throws -> Dragon

Note: there is no specification for type of error the function actually can throw. This declaration simply states that the function can throw an instance of any type implementing ErrorType or is not throwing at all.

2. Invoking function that may throw errors

In order to invoke function you need to use try keyword, like this

try summonDefaultDragon()

this line should normally be present do-catch block like this

do {
    let dragon = try summonDefaultDragon() 
} catch DragonError.dragonIsMissing {
    // Some specific-case error-handling
} catch DragonError.notEnoughMana(let manaRequired) {
    // Other specific-case error-handlng
} catch {
    // Catch all error-handling
}

Note: catch clause use all the powerful features of Swift pattern matching so you are very flexible here.

You may decided to propagate the error, if your are calling a throwing function from a function that is itself marked with throws keyword:

func fulfill(quest: Quest) throws {
    let dragon = try summonDefaultDragon()
    quest.ride(dragon)
} 

Alternatively, you can call throwing function using try?:

let dragonOrNil = try? summonDefaultDragon()

This way you either get the return value or nil, if any error occurred. Using this way you do not get the error object.

Which means that you can also combine try? with useful statements like:

if let dragon = try? summonDefaultDragon()

or

guard let dragon = try? summonDefaultDragon() else { ... }

Finally, you can decide that you know that error will not actually occur (e.g. because you have already checked are prerequisites) and use try! keyword:

let dragon = try! summonDefaultDragon()

If the function actually throws an error, then you’ll get a runtime error in your application and the application will terminate.

3. Throwing an error

In order to throw an error you use throw keyword like this

throw DragonError.dragonIsMissing

You can throw anything that conforms to ErrorType protocol. For starters NSError conforms to this protocol but you probably would like to go with enum-based ErrorType which enables you to group multiple related errors, potentially with additional pieces of data, like this

enum DragonError: ErrorType {
    case dragonIsMissing
    case notEnoughMana(requiredMana: Int)
    ...
}

Main differences between new Swift 2 & 3 error mechanism and Java/C#/C++ style exceptions are follows:

  • Syntax is a bit different: do-catch + try + defer vs traditional try-catch-finally syntax.
  • Exception handling usually incurs much higher execution time in exception path than in success path. This is not the case with Swift 2.0 errors, where success path and error path cost roughly the same.
  • All error throwing code must be declared, while exceptions might have been thrown from anywhere. All errors are «checked exceptions» in Java nomenclature. However, in contrast to Java, you do not specify potentially thrown errors.
  • Swift exceptions are not compatible with ObjC exceptions. Your do-catch block will not catch any NSException, and vice versa, for that you must use ObjC.
  • Swift exceptions are compatible with Cocoa NSError method conventions of returning either false (for Bool returning functions) or nil (for AnyObject returning functions) and passing NSErrorPointer with error details.

As an extra syntatic-sugar to ease error handling, there are two more concepts

  • deferred actions (using defer keyword) which let you achieve the same effect as finally blocks in Java/C#/etc
  • guard statement (using guard keyword) which let you write little less if/else code than in normal error checking/signaling code.

Swift 1

Runtime errors:

As Leandros suggests for handling runtime errors (like network connectivity problems, parsing data, opening file, etc) you should use NSError like you did in ObjC, because the Foundation, AppKit, UIKit, etc report their errors in this way. So it’s more framework thing than language thing.

Another frequent pattern that is being used are separator success/failure blocks like in AFNetworking:

var sessionManager = AFHTTPSessionManager(baseURL: NSURL(string: "yavin4.yavin.planets"))
sessionManager.HEAD("/api/destoryDeathStar", parameters: xwingSquad,
    success: { (NSURLSessionDataTask) -> Void in
        println("Success")
    },
    failure:{ (NSURLSessionDataTask, NSError) -> Void in
        println("Failure")
    })

Still the failure block frequently received NSError instance, describing the error.

Programmer errors:

For programmer errors (like out of bounds access of array element, invalid arguments passed to a function call, etc) you used exceptions in ObjC. Swift language does not seem to have any language support for exceptions (like throw, catch, etc keyword). However, as documentation suggests it is running on the same runtime as ObjC, and therefore you are still able to throw NSExceptions like this:

NSException(name: "SomeName", reason: "SomeReason", userInfo: nil).raise()

You just cannot catch them in pure Swift, although you may opt for catching exceptions in ObjC code.

The questions is whether you should throw exceptions for programmer errors, or rather use assertions as Apple suggests in the language guide.

In this article, we start by looking at the likely sources of errors in an application, some ways to prevent errors from occurring and implementing error handling, thinking about resilience as well as usability. We will explore the error handling model built into Swift, covering the powerful tools, associated techniques and how to apply them to build robust applications.

Recoverable vs. unrecoverable errors

Recoverable errors are errors that may temporarily halt normal program execution but the program should be able to recover gracefully and continue executing without the loss of any vital functionality. Unrecoverable errors are those from which the program cannot recover and must shut down. These could be caused by bugs in program logic or other unexpected situations such as unavailability of a resource without which the program cannot execute in a meaningful manner.

In this article, we will be focusing on recoverable errors since normal error handling does not apply to unrecoverable errors. Given the unexpected nature of unrecoverable errors, it is hard to plan for them. One strategy could be to put appropriate checks at places where a potential unrecoverable error could occur. If a check fails, a crash sequence could be initiated to shut the program down in as orderly a fashion as possible. This could include informing user that the application has encountered a fatal error and must terminate, saving any unsaved state and information related to the unrecoverable error, either seeking the user’s consent to send crash-related information to the developer or displaying an error code or other information that the user could quote when reporting the issue.

Sources of errors

Our programs may encounter errors in various ways. Before we delve into the specifics of how to handle errors, let’s look at how they may come about. Getting a better understanding of sources of errors can help write code to try and avoid many errors in the first place.

  1. Invalid user input: A user may not be aware of what constitutes invalid input or may make a mistake while providing input, for instance, non-numeric input where a number is expected or a non-existent choice when choosing among options.
  2. Contextually inappropriate user input: A user may provide input which is not appropriate given the context in which the input is being sought. This can often be the case when prior choices or selections made by the user affect what can be considered valid input. For instance, if an application asks a user to choose her country, any country could constitute a valid input. However, if the application has already asked the user to select a region and she has selected Europe, then a country choice of Canada would be contextually inappropriate. Similarly, if the user has already selected a certain service, only locations where that service is offered would be appropriate choices.
  3. Unavailability of an external system: An external system could be temporarily unavailable or non-responsive. For instance, a network may have a temporary outage or may be busy. It is also possible that an external system such as a persistence mechanism may have failed.
  4. Unexpected behaviour of an external system: An external system may fail to provide the expected response or information. This could be the result of a malfunction, an unexpected configuration change in the external system, lack of the required credentials, etc.
  5. Internal inconsistency or violation of invariants: There could be instances where a combination of external inputs and internal processes, each of which may be valid in isolation, may put our program in an internally inconsistent state. We could also have cases where actions taken by individual modules of a larger system, which may be valid in the context of each module, may violate invariants at the overall system level.

Programs should be designed, wherever practicable, to try and prevent errors.

  1. Structuring and validating user input: Structuring user input with radio buttons, check boxes, slider controls, etc., can limit the type or range of input a user can provide. Even freeform input should be validated where appropriate. To guard against contextually inappropriate input, funnels can be created which use options selected earlier to limit the input a user can provide to a given prompt. To use our earlier example, if a user selects Europe as the region, only countries in Europe should be presented for the next selection. Similarly, if a certain type of service has been selected, only locations where that service is offered should be available for selection.
  2. Prompts and warnings: Users can be prompted to reconfirm input provided. This can be done for each screen or related information provided over various screens can be summarized for verification. A warning prompt may be appropriate before committing irreversible changes or before accessing an external system, such as a network, which may not always be available. To maintain a balance between error prevention and streamlined user experience, users can be given the ability to disable certain prompts and warnings. Alternatively, a program may be able to adapt based on the frequency of a particular error for a certain user or group of users.
  3. Instructions and context-sensitive help: Assisting users in navigating the application by providing appropriate instructions and context-sensitive help can greatly aid in minimizing instances of users selecting inappropriate options or providing erroneous input.
  4. Consistency: Consistency is important not only for enhancing user experience but also for minimizing errors. From consistent screen design, so the same action is triggered in the same or similar way, to standardizing how users navigate through the application, when and how prompts and warnings are displayed and how users can seek help, it is best to choose one style and stick to it.

Handling errors

Whilst we should always design applications with error prevention as an objective, errors are inevitable in any real-world application. A robust mechanism to gracefully recover from errors is thus an essential part of application design.

The elegance of code is in maintaining as light a touch as possible in handling errors. Unnecessary or unnecessarily complex error-handling code can obfuscate the natural flow of application logic. The normal pathway through the application should always be clear, with errors and exceptional processing handled as simply and as unobtrusively as possible.

1. User experience considerations

From a user experience perspective, it is important that error handling should blend as seamlessly as possible into the natural flow of the application. It should not stand out like something the application designer did not expect or had not planned for. User interface elements such as screens, dialogs, popups, etc., used to indicate and recover from errors should fit with the overall theme of the application. While it is natural to want to make error situations stand out in some way to capture user attention, user interface elements used to handle errors should not appear as an afterthought or as something bolted on after the application had been designed.

Error handling code should explain, in as much detail as is appropriate, why a certain error occurred, what the user should do to get back to the normal flow of the application and, optionally, what steps the user could take to prevent recurrence.

2. Where to put error handling code

It is generally good practice to handle errors as close to the source as practically possible. However, given the architecture of modern applications, it is desirable in most situations to push errors some way up the call chain to get to a place where it is appropriate to handle them.

The reason for not handling errors as soon as they occur has to do with the architectural goal of separating user interface control from domain logic and infrastructure. With this separation of concerns, an error encountered in the model layer, which contains domain logic and business rules, or in the infrastructure layer, which deals with networking, persistence, etc., must be captured in an appropriate way along with the information required to effectively respond to it.

The error and the associated information then needs to be propagated back up the call chain to a place where it is appropriate to handle the user interaction required to report and suitably recover from the error. It is important, however, not to unnecessarily propagate errors and to deal with them as close to the source as possible within the context of the architecture pattern being used.

Using optionals for simple error handling

There are situations where we expect a function, method, initializer or closure to return an instance of a type but we also want to account for the possibility of failure. This could be the result of calling a function that is expected but not guaranteed to return an instance of a type, or an initializer that may fail to create a valid instance under certain circumstances. For simple operations where the reason for the error can be discerned from the context, it can be quite efficient to return an optional wrapping an instance of the expected type when the operation succeeds and return nil to indicate an error.

Let’s consider a class that models a person. For the sake of simplicity, let’s assume we record just the name of each person. We want to ensure, however, that an instance is created only if the name provided is not blank. Otherwise, the calling code should be notified that an instance could not be created. To accomplish this, we can use a failable initializer, which will either return an optional wrapping a Person instance or return nil.

class Person {
    init?(named name: String) {
        guard !name.isEmpty else { return nil }
        self.name = name
    }
    
    let name: String
}

The calling code can safely unwrap the result using optional binding.

func personCreationTester(name: String) {
    if let person = Person(named: name) {
        print("Created person named (person.name)")
    } else {
        print("Person creation failed. Name is blank")
    }
}

personCreationTester(name: "Laura") // Created person named Laura
personCreationTester(name: "")      // Person creation failed. Name is blank

Use of optionals to indicate simple errors is a common feature in the Swift Standard Library as well. The Collection protocol, for instance, defines an index(of:) method that returns the first index where the specified value appears in the collection, returning nil if the value is not found.

Here, we demonstrate it with an array.

func arrayIndexTester(number: Int) {
    let numbers = [1, 2, 3]
    if let index = numbers.index(of: number) {
        print("Index of (number) is (index)")
    } else {
        print("(number) is not in the array")
    }
}

arrayIndexTester(number: 2)     // Index of 2 is 1
arrayIndexTester(number: 4)     // 4 is not in the array

This basic form of error handling works quite well for simple operations with a return value and a single known point of failure. However, a number of operations in real-world applications are more complex and may have multiple points of failure. The calling code would likely need to know why a certain operation failed to be able to take appropriate action.

For such cases, we turn to the error handling mechanism built into Swift.

Representing errors

Errors in Swift are modeled using a type that conforms to the Error protocol. It is an empty protocol so there are no properties or methods to implement. It just indicates that a type can be used for error handling.

Swift enums are particularly suited to modeling errors. We define an enum that conforms to Error. Each case of the enum represents one error case. Where required, associated values can be used to provide supplementary information related to the error. As an example, when an error is thrown because a value is outside the permitted range for an operation, associated values can be used to indicate the upper and lower bonds of the permitted range. This can help create error messages that explain to the user why the error occurred and how to take corrective action.

Another use of associated values could be to encapsulate errors thrown at a lower level of abstraction or by methods dealing with infrastructure or third-party APIs. If methods at a higher level of abstraction simply propagate errors thrown by a lower-level abstraction or by a call to infrastructure services, third-party APIs, etc., it would pollute the higher-level API and expose implementation details. Errors thrown by lower layers should normally be caught and errors appropriate to the abstraction thrown in their place, with associated values used, where required, to preserve the underlying error.

Let’s suppose we are working in a domain dealing with enrollment of students in courses offered by a university. Students must be registered before they can enroll in any course. Their basic information is captured at the time of registration. Students may become inactive for a certain period of time due to non-payment of fees, suspension, extended leave of absence, etc. Inactive students are not allowed to enroll in courses. All students have a current level of achievement, which is used to determine whether they are eligible to enroll for a certain course, each of which has a minimum required level of achievement.

An attempt to enroll a student in a course may fail for three reasons. The student may be inactive, the course may already be full, or the student may not meet the minimum required achievement level. We define an EnrollmentError enum to represent these three error cases.

enum EnrollmentError: Error {
    case inactiveStudent
    case courseFull
    case doesNotMeetMinLevel(Int)
}

Note the use of an associated value to provide supplementary information for the error case where the minimum achievement level is not met.

Throwing errors

Errors can be thrown using the keyword throw. For example, an error to indicate that a course is full would be thrown like this.

throw EnrollmentError.courseFull

Once an error has been thrown, it must be dealt with in some way. An error thrown inside a function can be handled in the same function. However, as noted in an earlier section, there is often a need to pass errors thrown inside a function up the call chain to a place where it is more appropriate to handle them. This can be done by adding the keyword throws to the function’s signature, immediately after the closing parenthesis of the parameter list and before the return arrow, if one is present. This has the effect of throwing errors, which are not handled by the same function, up the call chain to the scope from where the function was called. A function with the keyword throws in its signature is called a throwing function.

Swift functions, methods, closures and initializers can all throw errors. For the sake of brevity, in the remainder of this post, the term throwing function will be used to refer to any function, method, closure or initializer that can throw errors.

Continuing our example of enrolling students in courses, we define a class to model a student. For every student, we record the name and the current level of achievement. All newly created students are active by default. Each student also has an ID. In a real-world application, students may be assigned unique IDs that are meaningful from a domain perspective, reflecting information such as year of graduation, department code, etc. For our purposes, we just use an instance of UUID to generate a unique value for each student.

class Student {
    init(named name: String, level: Int) {
        self.name = name
        self.level = level
    }
    
    let id = UUID()
    let name: String
    var level: Int
    var active = true
}

Next, we define a class to model a course, which has properties to store the name of the course, minimum achievement level required to enroll in the course and course capacity. Each course also has a unique ID. As with students, while courses in a real application may have domain-relevant IDs, for our purposes, we just use an instance of UUID.

We also give the class a method to enroll students in the course. We define a lightweight Enrollment type to capture the IDs of the student and the course for each successful enrollment. The enroll(_:) method throws the appropriate error if the student attempting to enroll in a course does not meet the minimum achievement level for the course.

typealias Enrollment = (studentId: UUID, courseId: UUID)

class Course {
    init(named name: String, minLevel: Int, capacity: Int) {
        self.name = name
        self.minLevel = minLevel
        self.capacity = capacity
    }
    
    func enroll(_ student: Student) throws -> Enrollment {
        guard student.level >= minLevel else {
            throw EnrollmentError.doesNotMeetMinLevel(minLevel)
        }
        return (studentId: student.id, courseId: id)
    }
    
    let id = UUID()
    let name: String
    let minLevel: Int
    let capacity: Int
}

Note the keyword throws in the signature of the method.

Propagating errors

A function that calls a throwing function is not required to handle any errors that may be thrown. It can simply propagate the errors, i.e., pass them up the call chain, to be handled elsewhere. To be able to propagate errors, a function must itself be throwing. Inside a throwing function, we prefix any expression that may throw an error with the try keyword. An expression prefixed with try is called a try expression.

To demonstrate this, we define a Registrar class, which has a method to enroll a student in a course. It first checks if the student is inactive or if the course is already full and throws appropriate errors as required. It then uses a try expression to call the enroll(_:) method on the course instance and stores the resulting Enrollment in a list of valid enrollments. Since the function itself is throwing, any error that may be thrown in the try expression is simply propagated.

class Registrar {
    func enroll(_ student: Student, in course: Course) throws {
        guard student.active else {
            throw EnrollmentError.inactiveStudent
        }
        guard isNotFull(course) else {
            throw EnrollmentError.courseFull
        }
        let enrollment = try course.enroll(student)
        enrollments.append(enrollment)
    }
    
    private func isNotFull(_ course: Course) -> Bool {
        return enrollments.filter({ $0.courseId == course.id }).count < course.capacity
    }
    
    private var enrollments: [Enrollment] = []
}

Note that, from the perspective of any code calling the above method, it can throw all three cases of EnrollmentError. The fact that two of the error cases are thrown inside the method and one is just propagated is not relevant.

Catching errors

All errors thrown by our code must be handled somewhere. If an error is not handled and keeps on getting propagated, it will eventually bubble all the way up the call stack, causing the application to crash. It is advisable, therefore, to keep track of errors being thrown and to handle them as soon as it is appropriate to do so.

To handle errors, we first catch them by means of a do-catch statement. This consists of a do clause, within which any statement or function call that can throw an error must appear in a try expression. The do clause is immediately followed by one or more catch clauses, each of which may match one or more errors using pattern matching. A catch clause may also include a where clause, allowing more specific matching behaviour.

Here is the general form of the do-catch statement.

do {
    try expression
    statements
} catch pattern 1 {
    statements
} catch pattern 2 where condition {
    statements
} catch {
    statements
}

Note the last catch clause, which does not contain any pattern. This will match any error and bind it to a local constant named error.

If no error is thrown in a do clause, execution continues along the same path and all catch clauses which are part of the same do-catch statement are skipped. If an error is thrown by any try expression, no further code in the enclosing do clause is executed and the catch clauses are evaluated in the order in which they appear to see which one matches the error. The first catch clause that matches the error is executed and the remaining ones are skipped. If no catch clause matches the error, the error is transferred to the surrounding scope, where it must either be handled by an enclosing do-catch statement or propagated.

To demonstrate catching errors, we define a method to test enrolling students in a course. This method uses an instance of Registrar to try and enroll students, catching all the errors thrown and printing appropriate messages to the console.

func testEnroll(_ students: [Student], in course: Course, using registrar: Registrar) {
    for student in students {
        do {
            try registrar.enroll(student, in: course)
            print("Successfully enrolled (student.name) in "(course.name)"")
        } catch EnrollmentError.inactiveStudent {
            print("(student.name) is not an active student")
        } catch EnrollmentError.courseFull {
            print("Could not enroll (student.name). Course is full")
        } catch EnrollmentError.doesNotMeetMinLevel(let minLevel) {
            print("Could not enroll (student.name). Must at least be at level (minLevel)")
        } catch {
            print("Unknown error")
        }
    }
}

Note the last catch clause, without which this code will not compile. This may seem counter-intuitive since we have only one error type and all of its cases are matched by the first three catch clauses. It is important to bear in mind, however, that errors in Swift are not typed. All the compiler knows is that errors thrown will be of a type that conforms to Error. The only way it has to ensure that all error cases will be matched is to insist on having the mop-up catch clause.

Here is some code to test the enrollment process.

let swiftBasics = Course(named: "Swift Basics", minLevel: 5, capacity: 2)
var students = [
    Student(named: "Laura", level: 6),
    Student(named: "Chris", level: 5),
    Student(named: "Charles", level: 4),
    Student(named: "Paul", level: 7),
    Student(named: "Jenna", level: 8)]
students[1].active = false
var registrar = Registrar()
testEnroll(students, in: swiftBasics, using: registrar)
// Successfully enrolled Laura in "Swift Basics"
// Chris is not an active student
// Could not enroll Charles. Must at least be at level 5
// Successfully enrolled Paul in "Swift Basics"
// Could not enroll Jenna. Course is full

Of the five students we tried to enroll above, only Laura and Paul were successfully enrolled. Chris was denied because his active status was revoked and Charles fell short of the minimum required achievement level. Jenna met the course requirements but could not be enrolled because the course capacity had already been reached.

Selectively propagating errors

A function that calls a throwing function may catch certain errors and propagate others. Such a function must be a throwing function to be able to propagate errors that it does not handle. Errors that get propagated may include errors thrown in one or more try expressions enclosed in do-catch statements, which are not matched by any of the catch clauses, as well as errors thrown in one or more try expressions that appear outside do-catch statements.

To demonstrate a throwing function that selectively handles some errors, we modify the Registrar class to give it a list of students who can exceptionally be enrolled in a course, provided they are only one short of the minimum achievement level required. We add a do-catch statement to the enroll(_:in:) method to catch errors related to minimum achievement level, using a where clause to match achievement level errors only if the achievement level of the student is exactly one below the required minimum and the student is included in the list of exceptions. The catch block ensures that such students are also enrolled in the course. All other errors are propagated as before.

class Registrar {
    func enroll(_ student: Student, in course: Course) throws {
        guard student.active else {
            throw EnrollmentError.inactiveStudent
        }
        guard isNotFull(course) else {
            throw EnrollmentError.courseFull
        }
        var enrollment: Enrollment
        do {
            enrollment = try course.enroll(student)
        } catch EnrollmentError.doesNotMeetMinLevel(let minLevel) where qualifiesForException(student, minLevel) {
            enrollment = (studentId: student.id, courseId: course.id)
        }
        enrollments.append(enrollment)
    }
    
    private func isNotFull(_ course: Course) -> Bool {
        return enrollments.filter({ $0.courseId == course.id }).count < course.capacity
    }
    
    private func qualifiesForException(_ student: Student, _ minLevel: Int) -> Bool {
        return (minLevel - student.level) == 1 && exceptions.contains(student.id)
    }
    
    private var enrollments: [Enrollment] = []
    var exceptions: [UUID] = []
}

Let’s see how this works.

let unitTesting = Course(named: "Unit Testing", minLevel: 5, capacity: 2)
students = [
    Student(named: "Mary", level: 4),
    Student(named: "Pauline", level: 4)]
registrar.exceptions.append(students[1].id)
testEnroll(students, in: unitTesting, using: registrar)
// Could not enroll Mary. Must at least be at level 5
// Successfully enrolled Pauline in "Unit Testing"

Both Mary and Pauline have an achievement level one below the minimum requirement and as such they should not be allowed to register for the course. Pauline, however, has been added to the exceptions array in the Registrar instance. When the code above is run, the error related to minimum achievement level for Pauline is caught by the Registrar instance. In the case of Mary, the Registrar instance just propagates the error.

Try vs. try? vs. try!

So far, we have seen only the try keyword, which requires handling the error in some way, either by catching it with a do-catch statement or propagating it by marking the function as throwing. There are two other flavours of try that can be used to deal with errors without catching or propagating them, so they don’t need to appear inside a do-catch statement or a throwing function.

1. Converting errors to optionals with try?

The try? keyword can be used to convert an error into an optional. If an error is thrown inside a try? expression, the expression evaluates to nil. If there is no error, the expression evaluates to an optional wrapping the value of the expression.

This comes in handy in situations where a particular use case only requires knowing whether an error got thrown and not which error it was. It can also be useful, for instance, when using frameworks and third-party libraries where we may not be interested in the details of some of the errors thrown but only in whether an operation failed.

To demonstrate, we define an error type with a single error case. We also define two throwing functions, only one of which actually throws an error.

enum TestError: Error {
    case someError
}

func canThrowButDoesNot() throws -> String {
    return "My string"
}

func canAndDoesThrow() throws -> String {
    throw TestError.someError
}

We use the above functions to see the result of a try? expression, when the throwing function actually throws an error and when it does not.

print((try? canThrowButDoesNot()) ?? "nil")     // My string
print((try? canAndDoesThrow()) ?? "nil")        // nil

When an error does not get thrown, we get an optional wrapping a string, which we unwrap using the ?? (nil-coalescing) operator. When an error does get thrown, we get nil.

2. Asserting that errors will not occur with try!

The try! keyword can be used to assert that an error will not be thrown. If there is no error, execution continues normally. If, however, an error does get thrown in a try! expression, the application will crash.

We can test the use of try! using the same functions we used above.

// Using try! is dangerous. Will cause a crash if error is thrown
print(try! canThrowButDoesNot())    // My string
print(try! canAndDoesThrow())       // Crash!!!

When no error is thrown, we get a String (not an optional wrapping a String). When an error does get thrown, our application crashes and burns.

Using try! is akin to forced unwrapping of an optional, which I would advise avoiding altogether. Swift provides a plethora of language features and associated techniques to work safely with optionals. Using try? and working with the resulting optional can lead to safer and more robust code.

Even in cases where we want to assert that an error should only occur if the application is unable to continue executing, we should not use try! as it will abruptly crash the application. It is advisable in such scenarios to use try?. This way, we get the opportunity to terminate the application in as orderly a manner as possible, as already explained in the section dealing with unrecoverable errors.

Throws vs. rethrows

We have already seen that a function that contains code that throws errors but does not handle all possible errors must itself be a throwing function, to be able to propagate any errors that don’t get handled. But what about a function that does not itself contain code that throws errors but takes a throwing closure as a parameter. Such a function could be given as an argument a closure that can throw but also a closure that cannot throw.

If the argument is a closure that can throw, the function should only be called with one of the flavours of try. However, if the argument is a closure that cannot throw, we should be able to call the function normally. Marking such a function with throws is not appropriate and we need another way.

Swift provides an elegant solution to this problem with the keyword rethrows, which is used for functions that take throwing closures as parameters. It indicates that the function can throw if the closure passed as the argument can throw. In such a case, any call to the rethrowing function must be annotated with one of the flavours of try. However, it can be called like a normal function when the closure passed as the argument cannot throw.

To demonstrate this, we define a function that takes as its only parameter a closure that returns a string but can also throw. The body of the function simply executes the closure and returns the same string.

func throwingClosureExpecter(closure: () throws -> String) rethrows -> String {
    return try closure()
}

Note the rethrows keyword in the function signature immediately after the closing parenthesis of the parameter list and before the return arrow. This indicates that the function may or may not throw depending on whether a throwing closure is passed in as the argument.

We need to test the behaviour of this function under three scenarios: When the closure cannot throw, when the closure can throw but does not, and when the closure can and does throw. To do this, we will use the two throwing functions canThrowButDoesNot() and canAndDoesThrow() that we had defined in the last section. For the scenario where the closure cannot throw, we define the following function.

func cannotThrow() -> String {
    return "My string"
}

Now we can test the three scenarios.

print(throwingClosureExpecter(closure: cannotThrow))                            // My string
print((try? throwingClosureExpecter(closure: canThrowButDoesNot)) ?? "nil")     // My string
print((try? throwingClosureExpecter(closure: canAndDoesThrow)) ?? "nil")        // nil

In the first case, we pass in a closure that cannot throw so there is no need for any form of try and our expression simply prints out the string returned by the closure. In the second case, we pass in a closure that can throw so we use a try? expression. Since no error is actually thrown, we get an optional wrapping the string returned by the closure, which we unwrap using the nil coalescing operator. In the final case, we pass in a closure that can and does throw so we again use a try? expression. Since an error is actually thrown, we get nil.

Just to complete the demonstration, we pass in a closure that can throw to our rethrowing function and try calling it without any form of try. The result is a compiler error.

// Compiler error: Call can throw but is not marked with 'try'
print(throwingClosureExpecter(closure: canThrowButDoesNot))    // Does not compile
print(throwingClosureExpecter(closure: canAndDoesThrow))       // Does not compile

It does not matter whether the closure actually throws an error. If the closure can throw, the rethrowing function can also throw and it must be called using some form of try.

It is important to note that a rethrowing function can only throw errors thrown by a closure passed in as an argument. It cannot throw any errors of its own.

Performing clean-up actions with defer

When an error is thrown, the current scope is exited. This could result in skipping clean-up actions that would have been performed had execution continued along the normal path. A defer statement can be used to ensure that certain actions are performed before the current scope is exited. A defer statement can contain as many statements as required and one scope can contain multiple defer statements.

It is noteworthy that defer statements are executed regardless of whether the current scope is exited normally or early due to a return statement, an error being thrown, etc. Defer statements are recognized as they are encountered in the normal execution path. If an early exit gets triggered, only defer statements encountered up to that point are executed prior to exiting the scope. Therefore, all defer statements that need to be executed prior to a particular early exit should be placed before the point where that early exit may occur.

When order of execution is important, we must bear in mind that defer statements are executed in the reverse of the order in which they are encountered during the normal flow of execution. We can think of them as being stored in a stack rather than a queue. Moreover, a defer statement itself cannot cause an early exit, so should not include statements such as return and should not throw errors.

To see how defer works in practice, we define a throwing function that takes a boolean parameter, which determines whether it will throw an error. The function contains three defer statements, each of which just prints a string to the console to indicate that it was executed. Two of the three defer statements appear before the point at which an error may be thrown.

func throwerWithDefer(shouldThrow: Bool) throws {
    defer {
        print("First defer statement executed")
    }
    defer {
        print("Second defer statement executed")
    }
    print("Prior to throw")
    if shouldThrow {
        throw TestError.someError
    }
    print("After throw")
    defer {
        print("Third defer statement executed")
    }
}

We now execute the above function and see the order of execution, first when no error is thrown and then when an error is thrown.

try? throwerWithDefer(shouldThrow: false)
// Prior to throw
// After throw
// Third defer statement executed
// Second defer statement executed
// First defer statement executed

When no error is thrown, the function executes normally and all three defer statements are executed, in reverse order, before the function is exited.

try? throwerWithDefer(shouldThrow: true)
// Prior to throw
// Second defer statement executed
// First defer statement executed

When an error is thrown, it causes an early exit from the function so code that appears after the point at which the error is thrown does not get executed. The first two defer statements, which are placed before the point at which the error is thrown, get executed before the function is exited. The third defer statement, which is placed after the point at which the error is thrown, does not get executed.

Errors and polymorphism

Polymorphism allows us to program to interfaces rather then implementations. With object-oriented programming, this is made possible by inheritance; with protocol-oriented programming, by making types conform to protocols. For polymorphism and abstraction to work correctly, our subclasses and conforming types should comply with the Liskov Substitution Principle, which requires that we should be able to freely substitute any subclass instance where a superclass type is expected and, by extension, any conforming type instance where a protocol type is expected.

In the context of methods that can throw, this means the following:

  1. If a method in a class cannot throw, it cannot be overridden in a subclass by a throwing method. Similarly, a protocol requirement for a method that cannot throw cannot be satisfied with a throwing method. This is because code that calls a method that cannot throw does not need to handle or propagate errors. If the same method can be overridden by a subclass or implemented by a conforming type as a throwing method, it will break client code since it will end up calling a throwing method without being able to handle errors.
  2. If a method in a class can throw, it can be overridden in a subclass by throwing method as well as a method that cannot throw. Similarly, a protocol requirement for a throwing method can be satisfied with a throwing method as well as a method that cannot throw. This is because code that calls a throwing method must be able to handle or propagate errors and it should be able to deal with situations when an error is thrown and when it isn’t. Therefore, such a method can be overridden by a subclass or implemented by a conforming type as a throwing method or a method that cannot throw without breaking any client code.

Here is some code to demonstrate the above with protocols and structs. It can easily be rewritten for class inheritance as well.

protocol X {
    func someFunc()
}

// Compiler error: Type 'A' does not conform to protocol 'X'
struct A: X {
    func someFunc() throws {}
}

Our attempt to satisfy the protocol requirement for a method that cannot throw with a throwing method is met with a compiler error.

protocol Y {
    func anotherFunc() throws
}

struct B: Y {
    func anotherFunc() throws {}
}

struct C: Y {
    func anotherFunc() {}
}

A protocol requirement for a throwing method can be satisfied by a throwing method as well as a method that cannot throw.

Conclusion

Swift provides excellent error handling capabilities. We can represent various cases for each error, use the error cases to encapsulate related information, throw the errors, propagate them and, when appropriate, catch them using pattern matching. We can even convert errors to optionals, suppress them or simply assert that they will not occur. This can enable us to build robust and relatively unobtrusive error handling right into our applications.

It is important to remember though that we should always think about how to prevent errors in the first place. For this, we need to understand the sources of errors, both in a general sense and in ways that are specific to a particular application. The sweet spot lies in achieving the right balance between error prevention and error handling. This is not always easy and takes experience to get right, but the payoff in usability and resilience should be well worth the effort.

Thank you for reading! I always appreciate constructive comments and feedback. Please feel free to leave a comment in the comments section of this post or start a discussion on Twitter.

Update 10/02/16: This tutorial has been updated for Xcode 8 and Swift 3.

Swift Error Handling is Magical

Error handling in Swift has come a long way since the patterns in Swift 1 that were inspired by Objective-C. Major improvements in Swift 2 made the experience of handling unexpected states and conditions in your application more straightforward. These benefits continue in Swift 3, but there are no significant updates to error handling made in the latest version of the language. (Phew!)

Just like other common programming languages, preferred error handling techniques in Swift can vary, depending upon the type of error encountered, and the overall architecture of your app.

This tutorial will take you through a magical example involving wizards, witches, bats and toads to illustrate how best to deal with common failure scenarios. You’ll also look at how to upgrade error handling from projects written in earlier versions of the language and, finally, gaze into your crystal ball at the possible future of error handling in Swift!

Note: This tutorial assumes you’re familiar with Swift 3 syntax – particularly enumerations and optionals. If you need a refresher on these concepts, start with the What’s New in Swift 2 post by Greg Heo, and the other materials linked.

Time to dive straight in (from the the cauldron into the fire!) and discover the various charms of error handling in Swift 3!

Getting Started

There are two starter playgrounds for this tutorial, one for each section. Download Avoiding Errors with nil – Starter.playground and Avoiding Errors with Custom Handling – Starter.playground playgrounds.

Open up the Avoiding Errors with nil starter playground in Xcode.

Read through the code and you’ll see several classes, structs and enums that hold the magic for this tutorial.

Take note the following parts of the code:

protocol Avatar {
  var avatar: String { get }
}

This protocol is applied to almost all classes and structs used throughout the tutorial to provide a visual representation of each object that can be printed to the console.

enum MagicWords: String {
  case abracadbra = "abracadabra"
  case alakazam = "alakazam"
  case hocusPocus = "hocus pocus"
  case prestoChango = "presto chango"
}

This enumeration denotes magic words that can be used to create a Spell.

struct Spell {
  var magicWords: MagicWords = .abracadabra
}

This is the basic building block for a Spell. By default, you initialize its magic words to .abracadabra.

Now that you’re acquainted with the basics of this supernatural world, you’re ready to start casting some spells.

Why Should I Care About Error Handling?

“Error handling is the art of failing gracefully.”

–Swift Apprentice, Chapter 22 (Error Handing)

Good error handling enhances the experience for end users as well as software maintainers by making it easier to identify issues, their causes and their associated severity. The more specific the error handling is throughout the code, the easier issues are to diagnose. Error handling also lets systems fail in an appropriate way so as not to frustrate or upset users.

But errors don’t always need to be handled. When they don’t, language features let you avoid certain classes of errors altogether. As a general rule, if you can avoid the possibility of an error, take that design path. If you can’t avoid a potential error condition, then explicit handling is your next best option.

Avoiding Swift Errors Using nil

Since Swift has elegant optional-handling capabilities, you can completely avoid the error condition where you expect a value, but no value is provided. As a clever programmer, you can manipulate this feature to intentionally return nil in an error condition. This approach works best where you should take no action if you reach an error state; i.e. where you choose inaction over emergency action.

Two typical examples of avoiding Swift errors using nil are failable initializers and guard statements.

Failable Initializers

Failable initializers prevent the creation of an object unless sufficient information has been provided. Prior to the availability of these initializers in Swift (and in other languages!), this functionality was typically achieved via the Factory Method Pattern.

An example of this pattern in Swift can be seen in create::

  static func create(withMagicWords words: String) -> Spell? {
    if let incantation = MagicWords(rawValue: words) {
      var spell = Spell()
      spell.magicWords = incantation
      return spell
    }
    else {
      return nil
    }
  }

The above initializer tries to create a spell using the magic words provided, but if the words are not magical you return nil instead.

Inspect the creation of the spells at the very bottom of this tutorial to see this behavior in action:

Spell.create

While first successfully creates a spell using the magic words "abracadabra", "ascendio" doesn’t have the same effect, and the return value of second is nil. (Hey, witches can’t win all the time).

Oh Really?

Factory methods are an old-school programming style. There are better ways to achieve the same thing in Swift. You’ll update the Spell extension to use a failable initializer instead of a factory method.

Delete create(_words:) and replace it with the following:

init?(words: String) {
  if let incantation = MagicWords(rawValue: words) {
    self.magicWords = incantation
  }
  else {
    return nil
  }
}

Here you’ve simplified the code by not explicitly creating and returning the Spell object.

The lines that assign first and second now throw compiler errors:

let first = Spell.create(withMagicWords: "abracadabra")
let second = Spell.create(withMagicWords: "ascendio")

You’ll need to change these to use the new initializer. Replace the lines above with the following:

let first = Spell(words: "abracadabra")
let second = Spell(words: "ascendio")

After this, all errors should be fixed and the playground should compile without error. With this change your code is definitely tidier – but you can do even better! :]

Guard Statements

guard is a quick way to assert that something is true: for example, if a value is > 0, or if a conditional can be unwrapped. You can then execute a block of code if the check fails.

guard was introduced in Swift 2 and is typically used to (bubble, toil and trouble) bubble-up error handling through the call stack, where the error will eventually be handled. Guard statements allow early exit from a function or method; this makes it more clear which conditions need to be present for the rest of the processing logic to run.

To clean up Spell‘s failable initializer further, edit it as shown below to use guard:

init?(words: String) {
  guard let incantation = MagicWords(rawValue: words) else {
    return nil
  }
  self.magicWords = incantation
}

With this change, there’s no need need for an else clause on a separate line and and the failure case is more evident as it’s now at the top of the intializer. Also, the “golden path” is the least indented. The “golden path” is the path of execution that happens when everything goes as expected, i.e. no error. Being least indented makes it easy to read.

Note that the values of first and second Spell constants haven’t changed, but the code is more more streamlined.

Avoiding Errors with Custom Handling

Having cleaned up the Spell initializer and avoided some errors through the clever use of nil, you’re ready to tackle some more intricate error handling.

For the next section of this tutorial, open up Avoiding Errors with Custom Handling – Starter.playground.

Take note of the following features of the code:

struct Spell {
  var magicWords: MagicWords = .abracadbra

  init?(words: String) {
    guard let incantation = MagicWords(rawValue: words) else {
      return nil
    }
    self.magicWords = incantation
  }

  init?(magicWords: MagicWords) {
    self.magicWords = magicWords
  }
}

This is the Spell initializer, updated to match the work you completed in the first section of this tutorial. Also note the presence of the Avatar protocol, and a second failable initializer, which has been added for convenience.

protocol Familiar: Avatar {
  var noise: String { get }
  var name: String? { get set }
  init(name: String?)
}

The Familiar protocol will be applied to various animals (such as bats and toads) further down in the playground.

Note: For those unfamiliar with the term familiar, this is a witch’s or wizard’s magical animal sidekick, which usually has human-like qualities. Think Hedwig from Harry Potter, or the flying monkeys in the Wizard of Oz.

Owl

This clearly isn’t Hedwig, but still cute nonetheless, no?

struct Witch: Magical {
  var avatar = "*"
  var name: String?
  var familiar: Familiar?
  var spells: [Spell] = []
  var hat: Hat?

  init(name: String?, familiar: Familiar?) {
    self.name = name
    self.familiar = familiar

    if let s = Spell(magicWords: .prestoChango) {
      self.spells = [s]
    }
  }

  init(name: String?, familiar: Familiar?, hat: Hat?) {
    self.init(name: name, familiar: familiar)
    self.hat = hat
  }

  func turnFamiliarIntoToad() -> Toad {
    if let hat = hat {
      if hat.isMagical { // When have you ever seen a Witch perform a spell without her magical hat on ? :]
        if let familiar = familiar {   // Check if witch has a familiar
          if let toad = familiar as? Toad {  // If familiar is already a toad, no magic required
            return toad
          } else {
            if hasSpell(ofType: .prestoChango) {
              if let name = familiar.name {
                return Toad(name: name)
              }
            }
          }
        }
      }
    }
    return Toad(name: "New Toad")  // This is an entirely new Toad.
  }

  func hasSpell(ofType type: MagicWords) -> Bool { // Check if witch currently has an appropriate spell in their spellbook
    let change = spells.flatMap { spell in
      spell.magicWords == type
    }
    return change.count > 0
  }
}

Finally, the witch. Here you see the following:

  • A Witch is initialized with a name and a familiar, or with a name, a familiar and a hat.
  • A Witch knows a finite number of spells, stored in spells, which is an array of Spell objects.
  • A Witch seems to have a penchant for turning her familiar into a toad via the use of the .prestoChango spell, within turnFamiliarIntoToad().

Notice the length and amount of indentation in turnFamiliarIntoToad(). Also, if anything goes wrong in the method, an entirely new toad will be returned. That seems like a confusing (and incorrect!) outcome for this particular spell. You’ll clean up this code significantly with custom error handling in the next section.

Refactoring to Use Swift Errors

Not to be confused with the Temple of Doom, the Pyramid of Doom is an anti-pattern found in Swift and other languages that can require many levels of nested statements for control flow. It can be seen in turnFamiliarIntoToad() above – note the six closing parentheses required to close out all the statements, trailing down on a diagonal. Reading code nested in this way requires excessive cognitive effort.

Pyramid of Doom

Guard statements, as you’ve seen earlier, and multiple simultaneous optional bindings can assist with the cleanup of pyramid-like code. The use of a do-catch mechanism, however, eliminates the problem altogether by decoupling control flow from error state handling.

do-catch mechanisms are often found near the following, related, keywords:

  • throws
  • do
  • catch
  • try
  • defer
  • Error

To see these mechanisms in action, you are going to throw multiple custom errors. First, you’ll define the states you wish to handle by listing out everything that could possibly go wrong as an enumeration.

Add the following code to your playground above the definition of Witch:

enum ChangoSpellError: Error {
  case hatMissingOrNotMagical
  case noFamiliar
  case familiarAlreadyAToad
  case spellFailed(reason: String)
  case spellNotKnownToWitch
}

Note two things about ChangoSpellError:

  • It conforms to the Error protocol, a requirement for defining errors in Swift.
  • In the spellFailed case, you can handily specify a custom reason for the spell failure with an associated value.

Note: The ChangoSpellError is named after the magical utterance of “Presto Chango!” – frequently used by a Witch when attempting to change a familiar into a Toad).

OK, ready to make some magic, my pretties? Excellent. Add throws to the method signature, to indicate that errors may occur as a result of calling this method:

func turnFamiliarIntoToad() throws -> Toad {

Update it as well on the Magical protocol:

protocol Magical: Avatar {
  var name: String? { get set }
  var spells: [Spell] { get set }
  func turnFamiliarIntoToad() throws -> Toad
}

Now that you have the error states listed, you will rework the turnFamiliarIntoToad() method, one clause at a time.

Handling Hat Errors

First, modify the following statement to ensure the witch is wearing her all-important hat:

if let hat = hat {

…to the following:

guard let hat = hat else {
  throw ChangoSpellError.hatMissingOrNotMagical
}

Note: Don’t forget to remove the associated } at the bottom of the method, or else the playground will compile with errors!

The next line contains a boolean check, also associated with the hat:

if hat.isMagical {

You could choose to add a separate guard statement to perform this check, but it would be clearer to group the checks together on a single line. As such, change the first guard statement to match the following:

guard let hat = hat, hat.isMagical else {
  throw ChangoSpellError.hatMissingOrNotMagical
}

Now remove the if hat.isMagical { check altogether.

In the next section, you’ll continue to unravel the conditional pyramid.

Handling Familiar Errors

Next up, alter the statement that checks if the witch has a familiar:

if let familiar = familiar {

…to instead throw a .noFamiliar error from another guard statement:

guard let familiar = familiar else {
  throw ChangoSpellError.noFamiliar
}

Ignore any errors that occur for the moment, as they will disappear with your next code change.

Handling Toad Errors

On the next line, the code returns the existing toad if the Witch tries to cast the turnFamiliarIntoToad() spell on her unsuspecting amphibian, but an explicit error would better inform her of the mistake. Change the following:

if let toad = familiar as? Toad {
  return toad
}

…to the following:

if familiar is Toad {
  throw ChangoSpellError.familiarAlreadyAToad
}

Note the change from as? to is lets you more succinctly check for conformance to the protocol without necessarily needing to use the result. The is keyword can also be used for type comparison in a more general fashion. If you’re interested in learning more about is and as, check out the type casting section of The Swift Programming Language.

Move everything inside the else clause outside of the else clause, and delete the else. It’s no longer necessary!

Handling Spell Errors

Finally, the hasSpell(_ type:) call ensures that the Witch has the appropriate spell in her spellbook. Change the code below:

if hasSpell(ofType: .prestoChango) {
  if let name = familiar.name {
    return Toad(name: name)
  }
}

…to the following:

guard hasSpell(ofType: .prestoChango) else {
  throw ChangoSpellError.spellNotKnownToWitch
}

guard let name = familiar.name else {
  let reason = "Familiar doesn’t have a name."
  throw ChangoSpellError.spellFailed(reason: reason)
}

return Toad(name: name)

And now you can remove the final line of code which was a fail-safe. Remove this line:

return Toad(name: "New Toad")

You now have the following clean and tidy method, ready for use. I’ve provided a few additional comments to the code below, to further explain what the method is doing:

func turnFamiliarIntoToad() throws -> Toad {

  // When have you ever seen a Witch perform a spell without her magical hat on ? :]
  guard let hat = hat, hat.isMagical else {
    throw ChangoSpellError.hatMissingOrNotMagical
  }

  // Check if witch has a familiar
  guard let familiar = familiar else {
    throw ChangoSpellError.noFamiliar
  }

  // Check if familiar is already a toad - if so, why are you casting the spell?
  if familiar is Toad {
    throw ChangoSpellError.familiarAlreadyAToad
  }
  guard hasSpell(ofType: .prestoChango) else {
    throw ChangoSpellError.spellNotKnownToWitch
  }

  // Check if the familiar has a name
  guard let name = familiar.name else {
    let reason = "Familiar doesn’t have a name."
    throw ChangoSpellError.spellFailed(reason: reason)
  }

  // It all checks out! Return a toad with the same name as the witch's familiar
  return Toad(name: name)
}

You could have returned an optional from turnFamiliarIntoToad() to indicate that “something went wrong while this spell was being performed”, but using custom errors more clearly expresses the error states and lets you react to them accordingly.

What Else Are Custom Errors Good For?

Now that you have a method to throw custom Swift errors, you need to handle them. The standard mechanism for doing this is called the do-catch statement, which is similar to try-catch mechanisms found in other languages such as Java.

Add the following code to the bottom of your playground:

func exampleOne() {
  print("") // Add an empty line in the debug area

  // 1
  let salem = Cat(name: "Salem Saberhagen")
  salem.speak()

  // 2
  let witchOne = Witch(name: "Sabrina", familiar: salem)
  do {
    // 3
    try witchOne.turnFamiliarIntoToad()
  }
  // 4
  catch let error as ChangoSpellError {
    handle(spellError: error)
  }
  // 5
  catch {
    print("Something went wrong, are you feeling OK?")
  }
}

Here’s what that function does:

  1. Create the familiar for this witch. It’s a cat called Salem.
  2. Create the witch, called Sabrina.
  3. Attempt to turn the feline into a toad.
  4. Catch a ChangoSpellError error and handle the error appropriately.
  5. Finally, catch all other errors and print out a nice message.

After you add the above, you’ll see a compiler error – time to fix that.

handle(spellError:) has not yet been defined, so add the following code above the exampleOne() function definition:

func handle(spellError error: ChangoSpellError) {
  let prefix = "Spell Failed."
  switch error {
    case .hatMissingOrNotMagical:
      print("(prefix) Did you forget your hat, or does it need its batteries charged?")

    case .familiarAlreadyAToad:
      print("(prefix) Why are you trying to change a Toad into a Toad?")

    default:
      print(prefix)
  }
}

Finally, run the code by adding the following to the bottom of your playground:

exampleOne()

Reveal the Debug console by clicking the up arrow icon in the bottom left hand corner of the Xcode workspace so you can see the output from your playground:

Expand Debug Area

Catching Errors

Below is a brief discussion of each of language feature used in the above code snippet.

catch

You can use pattern matching in Swift to handle specific errors or group themes of error types together.

The code above demonstrates several uses of catch: one where you catch a specific ChangoSpell error, and one that handles the remaining error cases.

try

You use try in conjunction with do-catch statements to clearly indicate which line or section of code may throw errors.

You can use try in several different ways:

  • try: standard usage within a clear and immediate do-catch statement. This is used above.
  • try?: handle an error by essentially ignoring it; if an error is thrown, the result of the statement will be nil.
  • try!: similar to the syntax used for force-unwrapping, this prefix creates the expectation that, in theory, a statement could throw an error – but in practice the error condition will never occur. try! can be used for actions such as loading files, where you are certain the required media exists. Like force-unwrap, this construct should be used carefully.

Time to check out a try? statement in action. Cut and paste the following code into the bottom of your playground:

func exampleTwo() {
  print("") // Add an empty line in the debug area
    
  let toad = Toad(name: "Mr. Toad")
  toad.speak()
    
  let hat = Hat()
  let witchTwo = Witch(name: "Elphaba", familiar: toad, hat: hat)
    
  print("") // Add an empty line in the debug area
    
  let newToad = try? witchTwo.turnFamiliarIntoToad()
  if newToad != nil { // Same logic as: if let _ = newToad
    print("Successfully changed familiar into toad.")
  }
  else {
    print("Spell failed.")
  }
}

Notice the difference with exampleOne. Here you don’t care about the output of the particular error, but still capture the fact that one occurred. The Toad was not created, so the value of newToad is nil.

Propagating Errors

throws

The throws keyword is required in Swift if a function or method throws an error. Thrown errors are automatically propagated up the call stack, but letting errors bubble too far from their source is considered bad practice. Significant propagation of errors throughout a codebase increases the likelihood errors will escape appropriate handling, so throws is a mandate to ensure propagation is documented in code – and remains evident to the coder.

rethrows

All examples you’ve seen so far have used throws, but what about its counterpart rethrows ?

rethrows tells the compiler that this function will only throw an error when its function parameter throws an error. A quick and magical example can be found below (no need to add this to the playground):

func doSomethingMagical(magicalOperation: () throws -> MagicalResult) rethrows -> MagicalResult {
  return try magicalOperation()
}

Here doSomethingMagical(_:) will only throw an error if the magicalOperation provided to the function throws one. If it succeeds, it returns a MagicalResult instead.

Manipulating Error Handling Behavior

defer

Although auto-propagation will serve you well in most cases, there are situations where you might want to manipulate the behavior of your application as an error travels up the call stack.

The defer statement is a mechanism that permits a ‘cleanup’ action to be performed whenever the current scope is exited, such as when a method or function returns. It’s useful for managing resources that need to be tidied up whether or not the action was successful, and so becomes especially useful in an error handling context.

To see this in action, add the following method to the Witch structure:

func speak() {
  defer {
    print("*cackles*")
  }
  print("Hello my pretties.")
}

Add the following code to the bottom of the playground:

func exampleThree() {
  print("") // Add an empty line in the debug area

  let witchThree = Witch(name: "Hermione", familiar: nil, hat: nil)
  witchThree.speak()
}

exampleThree()

In the debug console, you should see the witch cackle after everything she says.

Interestingly, defer statements are executed in the opposite order to which they are written.

Add a second defer statement to speak() so that a Witch screeches, then cackles after everything she says:

func speak() {
  defer {
    print("*cackles*")
  }

  defer {
    print("*screeches*")
  }

  print("Hello my pretties.")
}

Did the statements print in the order you expected? Ah, the magic of defer!

More Fun with Errors

The inclusion of the above statements in Swift brings the language into line with many other popular languages and separates Swift from the NSError-based approach found in Objective-C. Objective-C errors are, for the most part, directly translated, and the static analyzer in the compiler is excellent for helping you with which errors you need to catch, and when.

Although the do-catch and related features have significant overhead in other languages, in Swift they are treated like any other statement. This ensures they remain efficient – and effective.

But just because you can create custom errors and throw them around, doesn’t necessarily mean that you should. You really should develop guidelines regarding when to throw and catch errors for each project you undertake. I’d recommend the following:

  • Ensure error types are clearly named across your codebase.
  • Use optionals where a single error state exists.
  • Use custom errors where more than one error state exists.
  • Don’t allow an error to propagate too far from its source.

The Future of Error Handling in Swift

A couple of ideas for advanced error handling are floating around various Swift forums. One of the most-discussed concepts is untyped propagation.

“…we believe that we can extend our current model to support untyped propagation for universal errors. Doing this well, and in particular doing it without completely sacrificing code size and performance, will take a significant amount of planning and insight.”

– from Swift 2.x Error Handling

Whether you enjoy the idea of a major error handling change in a future version of Swift, or are happy with where things are today, it’s nice to know that clean error handling is being actively discussed and improved as the language develops.

Excellent

Where To Go From Here?

You can download the finished set of playgrounds here for this tutorial.

For further reading, I recommend the following articles, some of which have already been referenced throughout this tutorial:

  • Swift Apprentice, Chapter 22 – Error Handling
  • Failable Initializers
  • Factory Method Pattern
  • Pyramid of Doom

If you’re keen to see what may lie ahead in Swift, I recommend reading through the currently open proposals; see Swift Language Proposals for further details. If you’re feeling adventurous, why not submit your own? :]

Hopefully by now that you’ve been truly enchanted by error handling in Swift. If you have any questions or comments on this tutorial, please join the forum discussion below!

Понравилась статья? Поделить с друзьями:
  • Error handling response typeerror cannot read properties of undefined reading direction
  • Error handling nestjs
  • Error handling jsf
  • Error handling in node js
  • Error handling file saving did the server never start