Error type in 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.

Error Handling in Swift

Swift 3.0 Compliant

Regardless of your skill, handling errors is a common task for any developer. As developers we have complete control of the code we write and features we include, but despite our best efforts, we don’t control everything. We don’t control the values our users enter or whether our applications have access to the resources they need. In this post we’re going to look at how we can handle these situations by looking at error handling in Swift.

Error Conditions

In Swift terminology an error condition is:

…a failed condition check that results in a portion of our code skipping its usual course of execution.”

As such error conditions generally fall into one of three camps:

  • Logical Error Conditions
  • Simple Error Conditions
  • Recoverable Error Conditions

Let’s look at each of these in turn.

Logical Error Conditions

Logical error conditions are error conditions that are introduced as a result of the code we write. This category of error condition include things like accidentally calling a method on a variable we think is holding an object when the variable is in fact nil or trying to access an element on a collection using an out-of-range subscript.

For the most part, the Swift language (with the help of the compiler), does its best to help us avoid these kinds of error condition but it is not perfect and when these types of error conditions do occur it usually leads to our apps crashing.

Simple Error Conditions

The next category of error conditions are the simple error conditions. Simple error conditions are errors that occur as a result of performing some kind of operation that can fail in some obvious way. The error conditions in this category are usually simple in nature and because of this they are usually easily understandable and normally don’t need much (if any), additional information to understand what went wrong.

Converting a String into an Int is one such example:

let value : Int? = Int("1") // 1
let value2 : Int? = Int("Hello") // nil

A String value can either be converted to Int or it can’t. There’s no real grey area and we don’t need any additional information to work out what went wrong.

In Swift we commonly handle simple error conditions using optional values – returning the desired value in the success case or nil when an error condition is encountered.

Recoverable Error Conditions

Our final category of error conditions are the recoverable error conditions. Recoverable error conditions are error conditions that result from operations that fail in more complex ways and are going to be the main focus of this article.

Take the example of accessing the contents of a file on disk:

func contentsOf(file filename: String) -> String? {
    // ...
}

Although here the contentsOfFile(file:) method has been defined to return an optional value (NSString?), in reality there are a whole range of reasons that the function may have failed.

What if the file doesn’t exist? What if we don’t have permissions to access the file? What if the file is in an incompatible format?

Although the optional return value tells us whether an error condition was encountered, there just isn’t enough information to diagnose which error condition occurred.

What we need then is a mechanism that we can use to not only indicate that an error has occurred but one that also allow us to encapsulate additional information about what went wrong so we can react to the different error conditions.

It turns out that in Swift, there are a couple of ways we can do this but before we take a look at them let’s first take a look at how errors are actually represented in Swift.

The ErrorProtocol Protocol

In Swift, an error is a value used to report that an error condition has occurred. Error values in Swift are represented using value types that conform to the ErrorProtocol protocol (this was renamed from the ErrorType protocol in Swift 2.x).

The ErrorProtocol protocol is actually an empty protocol – it doesn’t require conforming types to implement any methods nor does it require them to have any particular properties all a type has to do is declare its conformance.

Now if you’ve done any sort of Cocoa programming before, it won’t surprise you to find out that the NSError class from the Foundation Framework already conforms to the ErrorProtocol protocol. However, Swift also allows us to go beyond this and define error types of our own.

To define an error type of our own in Swift, all we have to do is declare a new type that conforms to the ErrorProtocol protocol. Although we can do this with any Swift type (such as structs or classes), most commonly we use enumeration types for this task. Enumerations types are actually well suited to this for a number of reasons.

First they can be used to group together a set of related error values.

Take our file access example. We can define an new enumeration type that conforms to the ErrorProtocol protocol and groups together the different error conditions we may encounter when accessing the contents of a file:

enum FileError : ErrorProtocol {
case notFound
case permissionDenied
case unknownFormat
}

In this example the new error type is called FileError. As required it conforms to the ErrorProtocol protocol and has three potential values:

  • .notFound for when the file doesn’t exist,
  • .permissionDenied when we don’t have permission to access the file
  • .unknownFormat for when the file has an unknown or incompatible format.

Secondly, we can also, should we need to, make use of enumeration cases associated values to provide additional information about the nature of any error should it occur.

Ok, so we know how to represent errors but how do we solve our earlier problem and not only indicate that an error has occurred but also communicate which error occurred? Well, one option is to encapsulate our new error type within a result type.

Using Result Types

In previous posts, We’ve talked about optional values and we’ve seen how they are, under the hood, just a generic enumeration type with two values:

enum Optional<A> {
case some(A)
case none
}

Result types are actually very similar. A result type is a generic enumeration type that also has two cases – a failure case with an associated value of a type conforming to the ErrorProtocol protocol, and a success case with an associated value of the desired result type. In code it looks something like this:

enum ResultType {
case failure(ErrorProtocol)
case success(T)
}

Let’s apply this idea to our previous example.

Remember in our previous example we were returning an optional string (String?) to indicate the success or failure of our file access operation:

func contentsOf(file filename: String) -> String? {
    // ...
}

We can instead, re-define our function to return a ResultType value that is parameterised over the String type:

func contentsOf(file filename: String) -> ResultType {
    //...
}

Given our definition of the ResultType, any code now using the function can check the return value to see whether the function completed successfully or encountered an error. In addition, if an error did occur we can also determine exactly what the error was:

let filename = "source.txt"
let result = contentsOf(file: filename)

switch result {
case let .success(content):
    print(content)
case let .failure(error):
    switch error {
    case FileError.notFound:
        print("Unable to find file (filename).")
    case FileError.permissionDenied:
        print("You do not have permission to access the file (filename).")
    case FileError.unknownFormat:
        print("Unable to open file (filename) - incompatible format.")
    default:
        print("Unknown error")
    }
}

Note: Notice here that we have to include the default case in our error switch statement. As with all switch statements in Swift, the switch statement that checks the error values must be exhaustive. We must therefore ensure that we handle all values of type ErrorProtocol.

We can however, constrain things a little further.

If we tweak the ResultType to take two generic parameters instead of one, we can provide a type for both the associated value of the success case and for the associated value of the failure case:

enum ResultType {
case failure(E)
case success(T)
}

If we then parameterise our generic type over our more constrained FileError type as well as the String type:

func contentsOf(file filename: String) -> ResultType {
    //…
    return Result.failure(.notFound)
}

We can remove the need for our default case as the compiler knows that the errors can only be one of the three values we defined in our FileError type:

let filename = "source.txt"
let result = contentsOf(file: filename)

switch result {
case let .success(content):
    print(content)
case let .failure(error):
    switch error {
    case .notFound:
        print("Unable to find file (filename).")
    case .permissionDenied:
        print("You do not have permission to access the file (filename).")
    case .unknownFormat:
        print("Unable to open file (filename) - incompatible format.")
    }
}

Now, result types are a useful tool to have in our repertoire but as of Swift 2.0 the Swift language has gained a new error handling mechanism of it’s own and one that is baked into the language.

Throwing Errors with throw

In Swift, when we want to indicate that an error condition has occurred we have two options. We can either define and return a result type as we have done above or we can throw an error using Swift’s in-built error handling mechanism.

Throwing an error is just like creating a result type, it allows us to create a value that represents the encountered error condition and potentially attach additional information to that error. It won’t surprise you to find out that we do this using a throw statement.

The throw statement consists of the throw keyword followed by an expression whose value conforms to the ErrorProtocol protocol. For example:

throw FileError.notFound

In Swift, throw statements can occur in the body of any function or method or within the body of any closure expression. (To make things easier I’m just going to refer to these all as functions for now). When executed, throw statements cause execution of the current program scope to immediately end and the thrown error value to be propagated to the enclosing scope.

Error Handling the 10,000ft View

Ok. So far so good. We know how to throw an error but what do we do when these error values are actually thrown?

Generally speaking errors are not what we want to encounter in our code and when errors do occur (and trust me, they will) it is up to us to write code to detect and handle those errors.

The act of handling an error in Swift is known as catching it. So we’ve got throwing to create and propagate an error and catching to detect and handle the error. Throw. Catch. Get it?

Anyway, exactly where and when we handle errors in our code is up to us but there is one golden rule – not handling errors – ignoring them and hoping they go away – is not an option. The Swift compiler is actually pretty strict on this topic and requires that we handle all errors at some point in our chain of function calls – so much so that it won’t compile our code if we don’t!

Ok, so we have to handle and catch errors when they’re thrown but how exactly do we do that?

In Swift, we have two main courses of action when it comes to catching and handling errors:

  1. Propagate the error to another function further up the call chain (a.k.a. make some other piece of code handle it).
  2. Handle the error within the scope of the current function.

Which approach we take is dependent on what makes most sense for the particular situation but in the next few sections, we’ll take a closer look at each so we can make a more informed decision about which to choose. Let’s start by looking at how to propagate errors.

Propagating Errors

Error Propagation with throws

In Swift, whether or not a function can propagate an error is baked into the functions signature.

By default, a function in Swift cannot propagate errors. This applies to all types of function whether they be global functions, methods or closures. It also means that we must, by default, write code to catch any errors that may occur and handle them within the body of a function in which they occur.

Note: In the following sections I again refer to functions but this equally applies to methods and closures as well.

Despite these rules, sometimes it’s just not possible to handle errors within the scope of the current function. Maybe we don’t have enough information to handle it at that level or maybe it just doesn’t makes sense to do so. Whatever the reason, there are occasions when we want to propagate errors that have occurred within a function back to the code that called that function.

To indicate that a function may propagate an error back to it’s calling scope, Swift requires that we annotate such functions with the throws (or as we’ll see shortly – rethrows) keyword. Functions that are annotated in this manner are unsurprisingly known as throwing functions.

To annotate a function with the throws (or rethrows) keyword we write the keyword after the parameters in the functions declaration and, if the function returns a value, before the return arrow (->). For example, we could re-write our fileContents(filename:) example as follows:

func contentsOf(file filename: String) throws -> String {
    //...
}

Calling Throwing Functions

In the previous section we looked at annotating a function with the throws (or as we’ll see later the rethrows) keyword in order to propagate errors from the function. The thing is, annotating the function in this manner also has a second purpose – it makes it crystal clear for anybody reading the signature of the function that that function may propagate an error back to it’s calling code.

However there is something missing. If we look at it from a different perspective, the perspective of the code that called the function, things are a little less clear:

let content = contentsOf(file: "test.txt")

Here we not really sure whether the function will propagate an error or not. This lack of clarity was recognised as an issue within the language and to make things clear from the perspective of the calling code, the Swift language also mandates that when we call a function that can propagate an error, we have to prefix that function call with the try keyword (or as we’ll see the try? or try! keywords). Here’s an example:

let content = try contentsOfFile(file: "test.txt")

The inclusion of the try keyword does two things. First it signals to the compiler that we know we are calling a function that may propagate an error and secondly (and probably more importantly), it makes it clear to anyone reading our code that the function being called may throw an error.

Ok, let’s look at a more advanced example.

Throwing Functions and Inheritance

Note: This section is a little more advanced and may be challenging if you haven’t got any previous experience of object-oriented design principles. Don’t feel disheartened by this though. Every great developers was at that point at one point or other. Just skip this section for now and come back to it later – I won’t be offended 😉

Still here? Good. Ok super-quick object-oriented design re-cap.

As you probably know, inheritance is an object-oriented concept where functionality in one class can inherit the behaviour and properties of a parent or super class. You’ll also probably know that as part of that inheritance mechanism, it is also possible for child classes to override the behaviour of the methods in the parent class.

Now, you might be wondering why I mention this, but it’s an important consideration when it comes to error handling in Swift.

Imagine the scenario. We have a parent class (Foo) that has some method doSomething():

class Foo {
    func doSomething() -> String {
        return "Foo"
    }
}

Notice that doSomething() method is not annotated with the throws keyword. As we’ve learnt this means that any errors that occur within the body of the doSomething() method, must be handled within that method and any code calling the function is safe in the knowledge that no errors will be propagated.

Now imagine we are overriding that method in the child class (Bar).

As we know, to override a method in the child class, the method in the child class must have the same signature as that of the parent class in order to override it. However, suppose we tried to annotate the method with the throws keyword:

class Bar : Foo {
    override func doSomething() throws -> String {
        return "Bar"
    }
}

The thing is, this code doesn’t compile.

In the context of error handling, Swift doesn’t allow us to override a non-throwing method in a parent class with a throwing method in a child class. The same rule applies when attempting to satisfy a protocol requirement for a non-throwing method with a method that actually throws.

If you think about it, in both cases it’s not that surprising.

Think about the code that might be calling the doSomething() method on the parent (Foo) class. None of the code is expecting the call to the method to result in an error being propagated, after all it’s not annotated with the throws keyword and we’ve already talked about how only methods annotated with throws can propagate errors. As we know though, due to the wonders of object-oriented polymorphism, an instance of a child class can be cast to an instance of a parent class. This makes things a little complicated.

Imagine if the child class Bar was cast to an instance of the parent class Foo and the doSomething() method was called. Code calling that function would not expect the code to throw an error but the method of the underlying class (the child class Bar) could actually throw – a real surprise to any of the calling code. The Swift language doesn’t like nasty surprises and to avoid this whole problem, overriding in this way is not allowed in Swift.

However, consider this scenario.

Imagine we had another class (Baz) with a method that was already marked as throwing:

class Baz {
    func doSomethingElse() throws -> String {
        return "Foo"
    }
}

Now imagine we had a child class (Qux) that inherited from Baz and overrode the doSomethingElse() method:

class Qux : Baz {
    override func doSomethingElse() -> String {
        return "Bar"
    }
}

Notice here that the doSomethingElse() method is NOT marked as throwing in the child class. It might surprise you but this code does compile and is in fact perfectly legal.

If you think about it this again pretty logical. Consider the calling code again.

Any code that was calling the doSomethingElse() method in the parent (Baz) class already knows that there is a potential for the doSomethingElse() method to propagate an error. That means that that code will have been written to the cases where either an error is propagated from the method call or no error occurs.

If, as in our previous scenario, the child class (Qux) is then cast to an instance of the parent class (Baz) and the doSomethingElse() method is called, the fact that the doSomethingElse() method doesn’t raise an error is already handled by within the calling code. No surprises and no problems.

If we abstract this away a little, the point is that in Swift, functions that cannot throw, are actually subsets of functions that can throw. This means that we can use a non-throwing function, anywhere we can use a throwing function but not the reverse.

Propagating Errors with rethrows

Now that we’re clear about the relationship between throwing functions and inheritance, let’s look at a slightly different scenario.

Imagine we had a simple logging function log that takes a single parameter – a closure that returns the string to be logged:

func log(description:() -> String) -> Void {
    print(description())
}

In this form, everything works fine. However, now imagine that the closure we supplied to the log function could itself throw an error. What are the implications?

Well, if we called the closure within the body of the log function it obviously might result in an error being thrown. We could modify the log function to handle the error but equally we might want to propagate that error back to the calling function. This is where rethrows comes in.

In Swift the rethrows keyword is used to indicate that a function throws an error if and only if, one of its parameters throws an error. Functions or methods annotated in this manner are known as rethrowing functions or methods. Here’s an example:

func log(description: () throws -> String) rethrows -> () {
    print(try description())
}

func nonThrowing() -> String {
    return "Hello"
}

enum LogError : ErrorType {
    case someError
}

func throwing() throws -> String {
    // Do stuff....
    throw LogError.someError
}

log(nonThrowing)
try log(throwing) // Has to be called with 'try' because it might throw

In the first call to the log() function, the closure we supply as an argument (in this case the function nonThrowing()) doesn’t throw an error and therefore, due to the rethrows annotation, the log() function also doesn’t throw an error. This means we don’t need to prefix the call with the try keyword and are guaranteed that no error will be propagated.

Notice though that in the second call to the log() function, we do have to include the try keyword. In this case, because the closure we pass in as an argument throws an error, the log() function in turn throws an error. As a result of this, we therefore have to include the try keyword before the call to acknowledge that an error may be thrown.

Now, there are a few things to note with the rethrows keyword.

Firstly, rethrowing functions MUST take at least one throwing function as an argument. The compiler will complain if the don’t:

func baz(completion: () -> String) rethrows -> String {
    return completion()
} 
// ERROR: 'rethrows' function must take a throwing function argument

Secondly, throwing functions can only propagate errors that are thrown by the functions they take as arguments or errors that propagate from enclosed scopes. This means that rethrowing functions can’t contain any throw statements directly:

enum ExampleError : ErrorProtocol {
    case someError
    case someOtherError
}

func abc(completion: () -> String) rethrows -> String {
    throw ExampleError.someError
}
// ERROR: a function declared 'rethrows' may only throw if its parameter does

They can however catch errors using a do-catch statement (we’ll cover these shortly) and re-throw either the same or a different error which is then propagated to the enclosing scope:

func qux(completion: () throws -> String) rethrows -> String {
    do {
        return try completion()
    } catch {
        print("Inside Qux: (error)")
        throw ExampleError.someOtherError
    }
}

do {
    print(try qux { () throws -> String in 
        throw ExampleError.someError
    })
} catch {
    print("Outside Qux: (error)")
}

Note: If you’re a bit confused about the do-catch statement don’t worry, we’ll be looking at them in more detail shortly.

When it comes to rethrowing functions and inheritance, similar rules apply to those that we looked at earlier with throwing vs non-throwing functions.

Firstly, throwing methods can’t override rethrowing methods and throwing methods can’t satisfy protocol requirements for a rethrowing methods. This is for the same reasons as we looked at earlier. Code calling the throwing method knows whether the function will throw based on whether the functions supplied as an argument throw. If we overrode the function to always throw, the calling code might get some nasty surprises.

Conversely, rethrowing methods can override a throwing method and a rethrowing method can also satisfy a protocol requirement for a throwing method though. As we saw earlier, non-throwing methods are simply a subset of all throwing methods. This means that the calling code is already setup to handle any errors that may be thrown. If the method overridden method doesn’t throw a method then great, it doesn’t matter from the perspective of the calling code.

Handling Errors

Ok, let’s park error propagation for now and look at how we can actually handle errors rather than simply propagating the errors to an enclosing scope.

When it comes to handling errors we have three main options:

  • Handle the error in a do-catch statement.
  • Handle the error as an optional value.
  • Assert that the error will never occur.

As we’ve already touched on the do-catch statement in the previous section let’s start with that.

Handling Errors with do-catch Statement

In Swift, the do statement is used to introduce a new scope. It is similar to the curly braces ({}) in C and is used to delimit a new code block. The scope that the do statement introduces is just like any other scope. This means that variables and constants declared within that scope can only be accessed within that scope and go out of scope when the do statement ends. Now, you may be wondering why I mention the do statement, after all this is an article on error handling. The things is, the do statement has a trick up it’s sleeve. In addition to introducing a new scope, the do statement can optionally contain one or more catch clauses.

Each catch clause contains a pattern that can be used to match against defined error conditions. The catch clauses allows you to catch a thrown error value and handle that error.

From a high-level then, the do-catch statement has the following general form:

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

We can use this in various forms to to catch different types of error should they occur:

enum InputError : ErrorProtocol {
    case makeMissing
    case mileageTooLow(Int)
    case mileageTooHigh(Int)
}

func shouldBuyCar(make: String, mileage: Int) throws {
    guard make.characters.count > 0 else {
        throw InputError.makeMissing
    }

    switch mileage {
        case mileage where mileage < 10:
            throw InputError.mileageTooLow(mileage)
        case mileage where mileage > 100: 
            throw InputError.mileageTooHigh(mileage)
        default:
            print("Buy it!")
    }
}

do {
    try shouldBuyCar(make: "Honda", mileage:120)
}
catch InputError.makeMissing {
    print("Missing make")
}
catch let InputError.mileageTooHigh(x) where x > 150 {
    print("Mileage way way too high...")
}
catch let InputError.mileageTooHigh(x) {
    print("Mileage too high")
}
catch {
    print("(error)")
}
// Mileage too high

It’s a bit of a long example so let’s walk through it.

So, the scenario is that we’re buying a car and we have a little function that takes in some details and decides whether to buy the car or not. (Not the best way of doing it I’m sure but it’ll do for our example).

At the top, we then have the shouldBuyCar(make:mileage:) function. This function takes in the different parameters (the make and mileage) and decides whether we should buy the car. In this case the shouldBuyCar(make:mileage:) function is marked as throwing as it can potentially throw one or three errors.

  • If we supply an empty string for the cars make it will throw a .makeMissing error.
  • If the mileage is too low it will throw a .mileageToLow error and will attach the offending mileage as an associated value.
  • Finally if the mileage is too high it’ll throw a .mileageToHigh error again attaching the mileage that caused the issue.

In all other cases, it simply prints out the Buy it! statement to the console.

Now, the meat of what we want to look at is actually at the bottom of the example.

First, we have our do statement and within the body of the statement we make our call to the shouldBuyCar(make:mileage:) function prefixing it with the try keyword due to the fact that the function call may result in an error being thrown.

After that, we have a number of catch clauses. Each of these catch clauses contains a different pattern that will match against any error that is propagated from the call to the shouldBuyCar(make:mileage:) function.

First we use the identifier pattern to check for an explicit error value. This will match any and all makeMissing errors e.g.:

catch Car.InputError.makeMissing {
   // Handle the error.
   print("Missing make")
}

We also use an enumeration case pattern to match the .mileageTooHigh error value and extract any associated mileage:

catch InputError.mileageTooHigh(x) {
    print("Mileage too high")
}

We can also combine that with a where clause to further constrain things as we’ve done in the second catch clause:

catch let InputError.mileageTooHigh(x) where x > 150 {
    print("Mileage way way too high...")
}

Note: The catch clauses are evaluated sequentially, much like in a switch statement and will execute the first catch clause that matches. In this case we have to include our more contained catch clause containing the where clause before it’s more generic sibling if it is to ever match.

One other more subtle point to be aware of is that as with switch statements, the Swift compiler also attempts to infer whether the catch clauses in a do-catch statement are exhaustive. If a determination can be made, the compiler views the error has being handled. If however the catch clauses are not exhaustive, the error automatically propagates out of the containing scope, either to an enclosing catch clause or out of the function. As you might expect, if the error is propagated out of the function, the function must then be marked with the throws keyword to alert calling code of that possibility.

So that leaves the question of how to ensure that we have handled all potential errors.

We actually have a number of options for achieving this. First we can use a catch clause with a pattern that matches all errors, such as a wildcard pattern (_). The issue with this approach though is that we don’t get access the error value itself it is simply swallowed.

As an alternative, we can however use the catch clause on it’s own as I’ve done in the last catch clause in this example. When used in this manner the catch clause automatically matches and binds any error that it encounters to a local constant named error. This error value can then be accessed within the body of that catch clause.

Converting Errors Into Optional Values with try?

In addition to the do-catch statement, Swift also provides us with a couple of other options when it comes to handling errors. The first of these is the try? statement. We’ve already seen it’s sibling the try statement earlier in this article.

As you know, the try statement is used in front of calls to functions, methods or closures that may throw an error. try? is used in the same place however it’s behaviour is a little different. With the try? statement, if an error is thrown whilst evaluating the associated expression (such in a call to a throwing function) the result of the expression is set to nil rather than propagating the error back to the enclosing scope. Essentially this allows us to handle the error and convert it into an optional value in one move:

func throwingFunction() throws -> String {
    // ...
}

let result = try? throwingFunction()
// Result is an String?

As with other techniques we’ve seen though, this approach does have it’s shortcomings the main one being that you don’t get any information about the exact error that occurred. However, in certain situations, this may be just what you want so it’s worth knowing about.

Preventing Error Propagation with try!

Our final option when it comes to error handling is the try! statement. This is the more assertive version of it’s siblings.

Sometimes we are writing our code we know that a function won’t in fact throw an error at runtime despite the function being marked as such.

Maybe we have pre-validated the input data, maybe the circumstances are such that the error simply won’t be thrown. Whatever the reason, in these occasions we can use the try! keyword before a potentially throwing expression to disable the propagation of errors and wrap the call in a runtime assertion that no error will be thrown.

For example, say we had a function that threw an error if it was passed a negative value (and yes, I know we could define the parameter as UInt but go with it). We might choose to use the try! statement if we had already pre-validated that the argument we were passing to the function was definitely positive:

enum ValueError : ErrorProtocol {
    case negativeValue
}

func squarePositive(value: Int) throws -> Int {
    guard value >= 0 else {
        throw ValueError.negativeValue
    }
    return value * value
}

let output : Int
let input = 10
if input >= 0 {
    output = try! squarePositive(value: input)
    print(output)
}

A contrived example I know, but you get the idea. The only thing to mention is that you need to be a little careful with the try! statement. As I mentioned, the try! statement wraps the call to the throwing function in a run-time assertion. This means you need to be absolutely sure that no error will be thrown otherwise you risk your code crashing at runtime.

Error Propagation With Asynchronous Closures

The final thing I want to look at today is error handling and asynchronous operations. It’s another more advanced topic so skip this if you’re just getting started. Also, I’m not going to go into this in huge depth. This article is already pretty long but I wanted to mention it so that you had some idea about the possibilities. Anyway, let’s dip our toe in.

Asynchronous operations, as the name suggests, are where there is a delay between the initial call to a function and the response coming back. In Swift, this is commonly implemented through the use of completion handlers:

func asyncOperation(completion: String -> ()) -> Void {
    DispatchQueue.global(attributes: .qosDefault).async {
        // 1. Do stuff...
        let result = "Hello world"

        DispatchQueue.main.async {
            completion(result)
        }
    }
}

func completionHandler(result: String) {
    print(result)
}

asyncOperation(completion: completionHandler)
// Hello world

Note: If you’re trying the above example in a Playground you’ll also have to add the following at the top of your playground page to ensure the playground provides enough time for the asyncOperation(completion:) function to complete:

import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

Now, think about error handling and this example above. What if an error occurred within the body of the asynchronous operation? How could we handle it?

Well, with everything we’ve covered up until now, you should be armed with most of the tools you’d need. One option is to simply handle the error within the body of the asyncOperation(completion:) function by using a do-catch statement. Notice here that I also modify the completion handler to take an optional value which will be set to nil in the case of an error or the result otherwise:

enum ExampleError: ErrorProtocol {
    case error
}

func throwingOperation() throws -> String {
    throw ExampleError.error
}

func asyncOperation(operation:() throws -> String,
                    completion:String? -> ()) -> Void {
    DispatchQueue.global(attributes: .qosDefault).async {
        // Do stuff..
        var result: String? = nil
        do {
            result = try operation()
        } catch {
            print(error)
        }

        DispatchQueue.main.async {
            completion(result)
        }
    }
}

func completionHandler(result: String?) {
    print(result)
}

asyncOperation(operation: throwingOperation, completion: completionHandler)

Now this is fine, but what if we wanted to return any error that occurred back to the calling scope?

Well another option is to go back to the start of this article, and make use of the ResultType we talked about:

enum ExampleError: ErrorProtocol {
    case someError
}

enum ResultType<T> {
    case err(ErrorProtocol)
    case success(T)
}

func throwingOperation() throws -> String {
    throw ExampleError.someError
}

func asyncOperation(operation:() throws -> String,
                    completion:(ResultType<String>) -> ()) -> Void {
    DispatchQueue.global(attributes: .qosDefault).async {
        // Do stuff..
        var result: ResultType<String>
        do {
            let output = try operation()
            result = .success(output)
        } catch {
            result = .err(error)
        }

        DispatchQueue.main.async {
            completion(result)
        }
    }
}

func completionHandler(result: ResultType<String>) {
    switch result {
    case let .success(value):
        print(value)
    case let .err(error):
        print(error)
    }
}

asyncOperation(operation: throwingOperation, completion: completionHandler)

Another option, and one that is possible but feels like a bit of a hack is to us an inner closure:

enum ExampleError: ErrorProtocol {
    case someError
}

enum ResultType<T> {
    case err(ErrorProtocol)
    case success(T)
}

func throwingOperation() throws -> String {
    throw ExampleError.someError
}

func asyncOperation(operation:() throws -> String,
                    completion: (innerclosure: () throws -> String) -> Void) -> Void {

    DispatchQueue.global(attributes: .qosDefault).async {
        // Do stuff..
        var result: () throws -> String
        do {
            let output = try operation()
            result = { output }
        } catch {
            result = { throw error }
        }

        DispatchQueue.main.async {
            completion(innerclosure: result)
        }
    }
}

func completionHandler(result: () throws -> String) {

    do {
        let value = try result()
        print(value)
    } catch {
        print(error)
    }
}

asyncOperation(operation: throwingOperation, completion: completionHandler)

Here, instead of returning a ResultType we return a closure that encapsulates the result of the async operation. In the case of success, the returned closure will return the result. In case of error, the returned closure will (re?)throw the original error value. The calling code can then extract this result using Swift’s normal do-catch handling. The main downside with this inner-closure approach is that it doesn’t work with methods you don’t own. You’ll notice that in order to get this to work, the signature of the completion handler needs to be modified to accept a closure rather than a normal return value. This is works well if you own that code, but if you’re using one of the standard library functions then not so much. Despite this I thought it was worth mentioning as it’s good to have this encapsulated closure approach as an option.

Wrapping Up

Ok, that pretty much wraps it up for error handling in Swift.

We’ve looked at the different types of error that can occur, seen how to throw errors of our own and then spent the latter part of this article looking at how we can handle those errors when they do occur.

As we’ve seen, error handling took two major forms, either propagating errors or handling them using either the do-catch, try? or try! statements. Whatever your approach, hopefully this article will have given you a much better understanding of error handling in Swift and will allow you to start leveraging it’s power within your own code.

Creating custom error types is an important skill when designing clear APIs for iOS and macOS applications. This post presents examples for creating, extending, throwing, and catching custom error types in Swift:

  1. Create A Custom Error And Conform To The Error Type
  2. Extend A Custom Error
  3. description for Custom Errors Using CustomStringConvertible
  4. localizedDescription For Custom Errors Using LocalizedError
  5. Throw Custom Errors
  6. Catch Custom Errors

Create A Custom Error And Conform To The Error Type

To create a custom error, create an enum in Swift that conforms to the Error protocol. Each case of the enum represents a unique error that can be thrown and handled:

enum CustomError: Error {
    // Throw when an invalid password is entered
    case invalidPassword

    // Throw when an expected resource is not found
    case notFound

    // Throw in all other cases
    case unexpected(code: Int)
}

Extend A Custom Error

Like all Swift types, new custom error types you create can be extended to add computed properties and functions. In this example, the isFatal computed property is added that can be used to determine if the error is recoverable:

extension CustomError {
    var isFatal: Bool {
        if case CustomError.unexpected = self { return true }
        else { return false }
    }
}

description for Custom Errors Using CustomStringConvertible

Custom errors implemented in Swift can have custom descriptions for each error. To add a description to a new error type, extend the custom error to conform to CustomStringConvertible and add a property description:

// For each error type return the appropriate description
extension CustomError: CustomStringConvertible {
    public var description: String {
        switch self {
        case .invalidPassword:
            return "The provided password is not valid."
        case .notFound:
            return "The specified item could not be found."
        case .unexpected(_):
            return "An unexpected error occurred."
        }
    }
}

localizedDescription For Custom Errors Using LocalizedError

New custom errors you create in Swift can also have localized custom descriptions for each error. To add a localized description to a new error type, extend the custom error to conform to LocalizedError and add a property errorDescription:

// For each error type return the appropriate localized description
extension CustomError: LocalizedError {
    public var errorDescription: String? {
        switch self {
        case .invalidPassword:
            return NSLocalizedString(
                "The provided password is not valid.", 
                comment: "Invalid Password"
            )
        case .notFound:
            return NSLocalizedString(
                "The specified item could not be found.", 
                comment: "Resource Not Found"
            )
        case .unexpected(_):
            return NSLocalizedString(
                "An unexpected error occurred.", 
                comment: "Unexpected Error"
            )
        }
    }
}

Throw Custom Errors

Functions marked as throws in Swift can throw custom errors directly:

func isAvailable(resourcePath: String) throws {
    if FileManager.default.fileExists(atPath: path) { 
        return true 
    }
    else {
        throw CustomError.notFound
    }
}

Catch Custom Errors

Custom errors can be individually caught and handled using the do catch syntax. Use catch followed by the specific error to catch the error and apply specific handling logic:

func open(resourcePath: String) {
    do {
        try isAvailable(resourcePath: resourcePath)
        // Handle opening the resource
    }
    catch CustomError.notFound {
        // Handle custom error
    }
    catch {
        // Handle other errors
    }
}

Create Your Own Errors In Swift

That’s it! By conforming to Error, CustomStringConvertible, and LocalizedError you can implement descriptive, clear, and actionable custom errors in Swift.

As a tentpole feature for Swift 2.0, we are introducing a new
first-class error handling model. This feature provides standardized
syntax and language affordances for throwing, propagating, catching,
and manipulating recoverable error conditions.

Error handling is a well-trod path, with many different approaches in
other languages, many of them problematic in various ways. We believe
that our approach provides an elegant solution, drawing on the lessons
we’ve learned from other languages and fixing or avoiding some of the
pitfalls. The result is expressive and concise while still feeling
explicit, safe, and familiar; and we believe it will work beautifully
with the Cocoa APIs.

We’re intentionally not using the term “exception handling”, which
carries a lot of connotations from its use in other languages. Our
proposal has some similarities to the exceptions systems in those
languages, but it also has a lot of important differences.

Kinds of Error¶

What exactly is an “error”? There are many possible error conditions,
and they don’t all make sense to handle in exactly the same way,
because they arise in different circumstances and programmers have to
react to them differently.

We can break errors down into four categories, in increasing order of
severity:

A simple domain error arises from an operation that can fail in
some obvious way and which is often invoked speculatively. Parsing an
integer from a string is a really good example. The client doesn’t
need a detailed description of the error and will usually want to
handle the error immediately. These errors are already well-modeled
by returning an optional value; we don’t need a more complex language
solution for them.

A recoverable error arises from an operation which can fail in
complex ways, but whose errors can be reasonably anticipated in
advance. Examples including opening a file or reading from a network
connection. These are the kinds of errors that Apple’s APIs use
NSError for today, but there are close analogues in many other APIs,
such as errno in POSIX.

Ignoring this kind of error is usually a bad idea, and it can even be
dangerous (e.g. by introducing a security hole). Developers should be
strongly encouraged to write code that handles the error. It’s common
for developers to want to handle errors from different operations in
the same basic way, either by reporting the error to the user or
passing the error back to their own clients.

These errors will be the focus on this proposal.

The final two classes of error are outside the scope of this proposal.
A universal error is theoretically recoverable, but by its nature
the language can’t help the programmer anticipate where it will come
from. A logic failure arises from a programmer mistake and should
not be recoverable at all. In our system, these kinds of errors are
reported either with Objective-C/C++ exceptions or simply by
logging a message and calling abort(). Both kinds of error are
discussed extensively in the rationale. Having considered them
carefully, we believe that we can address them in a later release
without significant harm.

Aspects of the Design¶

This approach proposed here is very similar to the error handling
model manually implemented in Objective-C with the NSError
convention. Notably, the approach preserves these advantages of this
convention:

  • Whether a method produces an error (or not) is an explicit part of
    its API contract.
  • Methods default to not producing errors unless they are explicitly
    marked.
  • The control flow within a function is still mostly explicit: a
    maintainer can tell exactly which statements can produce an error,
    and a simple inspection reveals how the function reacts to the
    error.
  • Throwing an error provides similar performance to allocating an
    error and returning it – it isn’t an expensive, table-based stack
    unwinding process.
  • Cocoa APIs using standard NSError patterns can be imported into
    this world automatically. Other common patterns (e.g. CFError,
    errno) can be added to the model in future versions of Swift.

In addition, we feel that this design improves on Objective-C’s error
handling approach in a number of ways:

  • It eliminates a lot of boilerplate control-flow code for propagating
    errors.
  • The syntax for error handling will feel familiar to people used to
    exception handling in other languages.
  • Defining custom error types is simple and ties in elegantly with
    Swift enums.

As to basic syntax, we decided to stick with the familiar language of
exception handling. We considered intentionally using different terms
(like raise / handle) to try to distinguish our approach from
other languages. However, by and large, error propagation in this
proposal works like it does in exception handling, and people are
inevitably going to make the connection. Given that, we couldn’t find
a compelling reason to deviate from the throw / catch legacy.

This document just contains the basic proposal and will be very
light on rationale. We considered many different languages and
programming environments as part of making this proposal, and there’s
an extensive discussion of them in the separate rationale document.
For example, that document explains why we don’t simply allow all
functions to throw, why we don’t propagate errors using simply an
ErrorOr<T> return type, and why we don’t just make error propagation
part of a general monad feature. We encourage you to read that
rationale if you’re interested in understanding why we made the
decisions we did.

With that out of the way, let’s get to the details of the proposal.

Typed propagation¶

Whether a function can throw is part of its type. This applies to all
functions, whether they’re global functions, methods, or closures.

By default, a function cannot throw. The compiler statically enforces
this: anything the function does which can throw must appear in a
context which handles all errors.

A function can be declared to throw by writing throws on the
function declaration or type:

func foo() -> Int {  // This function is not permitted to throw.
func bar() throws -> Int {   // This function is permitted to throw.

throws is written before the arrow to give a sensible and consistent
grammar for function types and implicit () result types, e.g.:

func baz() throws {

// Takes a 'callback' function that can throw.
// 'fred' itself can also throw.
func fred(callback: (UInt8) throws -> ()) throws {

// These are distinct types.
let a : () -> () -> ()
let b : () throws -> () -> ()
let c : () -> () throws -> ()
let d : () throws -> () throws -> ()

For curried functions, throws only applies to the innermost
function. This function has type (Int) -> (Int) throws -> Int:

func jerry(i: Int)(j: Int) throws -> Int {

throws is tracked as part of the type system: a function value
must also declare whether it can throw. Functions that cannot throw
are a subtype of functions that can, so you can use a function that
can’t throw anywhere you could use a function that can:

func rachel() -> Int { return 12 }
func donna(generator: () throws -> Int) -> Int { ... }

donna(rachel)

The reverse is not true, since the caller would not be prepared to
handle the error.

A call to a function which can throw within a context that is not
allowed to throw is rejected by the compiler.

It isn’t possible to overload functions solely based on whether the
functions throw. That is, this is not legal:

func foo() {
func foo() throws {

A throwing method cannot override a non-throwing method or satisfy a
non-throwing protocol requirement. However, a non-throwing method can
override a throwing method or satisfy a throwing protocol requirement.

It is valuable to be able to overload higher-order functions based on
whether an argument function throws, so this is allowed:

func foo(callback: () throws -> Bool) {
func foo(callback: () -> Bool) {

rethrows

Functions which take a throwing function argument (including as an
autoclosure) can be marked as rethrows:

extension Array {
  func map<U>(fn: ElementType throws -> U) rethrows -> [U]
}

It is an error if a function declared rethrows does not include a
throwing function in at least one of its parameter clauses.

rethrows is identical to throws, except that the function
promises to only throw if one of its argument functions throws.

More formally, a function is rethrowing-only for a function f if:

  • it is a throwing function parameter of f,
  • it is a non-throwing function, or
  • it is implemented within f (i.e. it is either f or a function or
    closure defined therein) and it does not throw except by either:

    • calling a function that is rethrowing-only for f or
    • calling a function that is rethrows, passing only functions
      that are rethrowing-only for f.

It is an error if a rethrows function is not rethrowing-only for
itself.

A rethrows function is considered to be a throwing function.
However, a direct call to a rethrows function is considered to not
throw if it is fully applied and none of the function arguments can
throw. For example:

// This call to map is considered not to throw because its
// argument function does not throw.
let absolutePaths = paths.map { "/" + $0 }

// This call to map is considered to throw because its
// argument function does throw.
let streams = try absolutePaths.map { try InputStream(filename: $0) }

For now, rethrows is a property of declared functions, not of
function values. Binding a variable (even a constant) to a function
loses the information that the function was rethrows, and calls to
it will use the normal rules, meaning that they will be considered to
throw regardless of whether a non-throwing function is passed.

For the purposes of override and conformance checking, rethrows
lies between throws and non-throws. That is, an ordinary
throwing method cannot override a rethrows method, which cannot
override a non-throwing method; but an ordinary throwing method can be
overridden by a rethrows method, which can be overridden by a
non-throwing method. Equivalent rules apply for protocol conformance.

Throwing an error¶

The throw statement begins the propagation of an error. It always
take an argument, which can be any value that conforms to the
ErrorType protocol (described below).

if timeElapsed > timeThreshold {
  throw HomeworkError.Overworked
}

throw NSError(domain: "whatever", code: 42, userInfo: nil)

As mentioned above, attempting to throw an error out of a function not
marked throws is a static compiler error.

Catching errors¶

A catch clause includes an optional pattern that matches the
error. This pattern can use any of the standard pattern-matching
tools provided by switch statements in Swift, including boolean
where conditions. The pattern can be omitted; if so, a where
condition is still permitted. If the pattern is omitted, or if it
does not bind a different name to the error, the name error is
automatically bound to the error as if with a let pattern.

The try keyword is used for other purposes which it seems to fit far
better (see below), so catch clauses are instead attached to a
generalized do statement:

// Simple do statement (without a trailing while condition),
// just provides a scope for variables defined inside of it.
do {
   let x = foo()
}

// do statement with two catch clauses.
do {
  ...

} catch HomeworkError.Overworked {
  // a conditionally-executed catch clause

} catch _ {
  // a catch-all clause.
}

As with switch statements, Swift makes an effort to understand
whether catch clauses are exhaustive. If it can determine it is, then
the compiler considers the error to be handled. If not, the error
automatically propagates out out of scope, either to a lexically
enclosing catch clause or out of the containing function (which must
be marked throws).

We expect to refine the catch syntax with usage experience.

ErrorType

The Swift standard library will provide ErrorType, a protocol with
a very small interface (which is not described in this proposal). The
standard pattern should be to define the conformance of an enum to
the type:

enum HomeworkError : ErrorType {
  case Overworked
  case Impossible
  case EatenByCat(Cat)
  case StopStressingMeWithYourRules
}

The enum provides a namespace of errors, a list of possible errors
within that namespace, and optional values to attach to each option.

Note that this corresponds very cleanly to the NSError model of an
error domain, an error code, and optional user data. We expect to
import system error domains as enums that follow this approach and
implement ErrorType. NSError and CFError themselves will also
conform to ErrorType.

The physical representation (still being nailed down) will make it
efficient to embed an NSError as an ErrorType and vice-versa. It
should be possible to turn an arbitrary Swift enum that conforms to
ErrorType into an NSError by using the qualified type name as the
domain key, the enumerator as the error code, and turning the payload
into user data.

Automatic, marked, propagation of errors¶

Once an error is thrown, Swift will automatically propagate it out of
scopes (that permit it), rather than relying on the programmer to
manually check for errors and do their own control flow. This is just
a lot less boilerplate for common error handling tasks. However,
doing this naively would introduce a lot of implicit control flow,
which makes it difficult to reason about the function’s behavior.
This is a serious maintenance problem and has traditionally been a
considerable source of bugs in languages that heavily use exceptions.

Therefore, while Swift automatically propagates errors, it requires
that statements and expressions that can implicitly throw be marked
with the try keyword. For example:

func readStuff() throws {
  // loadFile can throw an error.  If so, it propagates out of readStuff.
  try loadFile("mystuff.txt")

  // This is a semantic error; the 'try' keyword is required
  // to indicate that it can throw.
  var y = stream.readFloat()

  // This is okay; the try covers the entire statement.
  try y += stream.readFloat()

  // This try applies to readBool().
  if try stream.readBool() {
    // This try applies to both of these calls.
    let x = try stream.readInt() + stream.readInt()
  }

  if let err = stream.getOutOfBandError() {
    // Of course, the programmer doesn't have to mark explicit throws.
    throw err
  }
}

Developers can choose to “scope” the try very tightly by writing it
within parentheses or on a specific argument or list element:

// Ok.
let x = (try stream.readInt()) + (try stream.readInt())

// Semantic error: the try only covers the parenthesized expression.
let x2 = (try stream.readInt()) + stream.readInt()

// The try applies to the first array element.  Of course, the
// developer could cover the entire array by writing the try outside.
let array = [ try foo(), bar(), baz() ]

Some developers may wish to do this to make the specific throwing
calls very clear. Other developers may be content with knowing that
something within a statement can throw. The compiler’s fixit hints will
guide developers towards inserting a single try that covers the entire
statement. This could potentially be controlled someday by a coding
style flag passed to the compiler.

try!

To concisely indicate that a call is known to not actually throw at
runtime, try can be decorated with !, turning the error check
into a runtime assertion that the call does not throw.

For the purposes of checking that all errors are handled, a try!
expression is considered to handle any error originating from within
its operand.

try! is otherwise exactly like try: it can appear in exactly
the same positions and doesn’t affect the type of an expression.

Manual propagation and manipulation of errors¶

Taking control over the propagation of errors is important for some
advanced use cases (e.g. transporting an error result across threads
when synchronizing a future) and can be more convenient or natural for
specific use cases (e.g. handling a specific call differently within a
context that otherwise allows propagation).

As such, the Swift standard library should provide a standard
Rust-like Result<T> enum, along with API for working with it,
e.g.:

  • A function to evaluate an error-producing closure and capture the
    result as a Result<T>.
  • A function to unpack a Result<T> by either returning its
    value or propagating the error in the current context.

This is something that composes on top of the basic model, but that
has not been designed yet and details aren’t included in this
proposal.

The name Result<T> is a stand-in and needs to be designed and
reviewed, as well as the basic operations on the type.

defer

Swift should provide a defer statement that sets up an ad hoc
clean-up action to be run when the current scope is exited. This
replicates the functionality of a Java-style finally, but more
cleanly and with less nesting.

This is an important tool for ensuring that explicitly-managed
resources are released on all paths. Examples include closing a
network connection and freeing memory that was manually allocated. It
is convenient for all kinds of error-handling, even manual propagation
and simple domain errors, but is especially nice with automatic
propagation. It is also a crucial part of our long-term vision for
universal errors.

defer may be followed by an arbitrary statement. The compiler
should reject a defer action that might terminate early, whether by
throwing or with return, break, or continue.

Example:

if exists(filename) {
  let file = open(filename, O_READ)
  defer close(file)

  while let line = try file.readline() {
    ...
  }

  // close occurs here, at the end of the formal scope.
}

If there are multiple defer statements in a scope, they are guaranteed
to be executed in reverse order of appearance. That is:

let file1 = open("hello.txt")
defer close(file1)
let file2 = open("world.txt")
defer close(file2)
...
// file2 will be closed first.

A potential extension is to provide a convenient way to mark that a
defer action should only be taken if an error is thrown. This is a
convenient shorthand for controlling the action with a flag. We will
evaluate whether adding complexity to handle this case is justified
based on real-world usage experience.

Importing Cocoa¶

If possible, Swift’s error-handling model should transparently work
with the SDK with a minimal amount of effort from framework owners.

We believe that we can cover the vast majority of Objective-C APIs
with NSError** out-parameters by importing them as throws and
removing the error clause from their signature. That is, a method
like this one from NSAttributedString:

- (NSData *)dataFromRange:(NSRange)range
       documentAttributes:(NSDictionary *)dict
                    error:(NSError **)error;

would be imported as:

func dataFromRange(range: NSRange,
                   documentAttributes dict: NSDictionary) throws -> NSData

There are a number of cases to consider, but we expect that most can
be automatically imported without extra annotation in the SDK, by
using a couple of simple heuristics:

  • The most common pattern is a BOOL result, where a false value
    means an error occurred. This seems unambiguous.

  • Also common is a pointer result, where a nil result usually
    means an error occurred. This appears to be universal in
    Objective-C; APIs that can return nil results seem to do so via
    out-parameters. So it seems to be safe to make a policy decision
    that it’s okay to assume that a nil result is an error by
    default.

    If the pattern for a method is that a nil result means it produced
    an error, then the result can be imported as a non-optional type.

  • A few APIs return void. As far as I can tell, for all of these,
    the caller is expected to check for a non-nil error.

For other sentinel cases, we can consider adding a new clang attribute
to indicate to the compiler what the sentinel is:

  • There are several APIs returning NSInteger or NSUInteger. At
    least some of these return 0 on error, but that doesn’t seem like a
    reasonable general assumption.
  • AVFoundation provides a couple methods returning
    AVKeyValueStatus. These produce an error if the API returned
    AVKeyValueStatusFailed, which, interestingly enough, is not the
    zero value.

The clang attribute would specify how to test the return value for an
error. For example:

+ (NSInteger)writePropertyList:(id)plist
                      toStream:(NSOutputStream *)stream
                        format:(NSPropertyListFormat)format
                       options:(NSPropertyListWriteOptions)opt
                         error:(out NSError **)error
  NS_ERROR_RESULT(0);

- (AVKeyValueStatus)statusOfValueForKey:(NSString *)key
                                  error:(NSError **)
  NS_ERROR_RESULT(AVKeyValueStatusFailed);

We should also provide a Clang attribute which specifies that the
correct way to test for an error is to check the out-parameter. Both
of these attributes could potentially be used by the static analyzer,
not just Swift. (For example, they could try to detect an invalid
error check.)

Cases that do not match the automatically imported patterns and that
lack an attribute would be left unmodified (i.e., they’d keep their
NSErrorPointer argument) and considered “not awesome” in the SDK
auditing tool. These will still be usable in Swift: callers will get
the NSError back like they do today, and have to throw the result
manually.

For initializers, importing an initializer as throwing takes
precedence over importing it as failable. That is, an imported
initializer with a nullable result and an error parameter would be
imported as throwing. Throwing initializers have very similar
constraints to failable initializers; in a way, it’s just a new axis
of failability.

One limitation of this approach is that we need to be able to reconstruct
the selector to use when an overload of a method is introduced. For this
reason, the import is likely to be limited to methods where the error
parameter is the last one and the corresponding selector
chunk is either error: or the first chunk (see below). Empirically,
this seems to do the right thing for all but two sets of APIs in the
public API:

  • The ISyncSessionDriverDelegate category on NSObject declares
    half-a-dozen methods like this:

    - (BOOL)sessionDriver:(ISyncSessionDriver *)sender
            didRegisterClientAndReturnError:(NSError **)outError;
    

    Fortunately, these delegate methods were all deprecated in Lion, and
    are thus unavailable in Swift.

  • NSFileCoordinator has half a dozen methods where the error:
    clause is second-to-last, followed by a block argument. These
    methods are not deprecated as far as I know.

The above translation rule would import methods like this one from
NSDocument:

- (NSDocument *)duplicateAndReturnError:(NSError **)outError;

like so:

func duplicateAndReturnError() throws -> NSDocument

The AndReturnError bit is common but far from universal; consider
this method from NSManagedObject:

- (BOOL)validateForDelete:(NSError **)error;

This would be imported as:

func validateForDelete() throws

This is a really nice import, and it’s somewhat unfortunate that we
can’t import duplicateAndReturnError: as duplicate().

Potential future extensions to this model¶

We believe that the proposal above is sufficient to provide a huge
step forward in error handling in Swift programs, but there is always
more to consider in the future. Some specific things we’ve discussed
(and may come back to in the future) but don’t consider to be core to
the Swift 2.0 model are:

Higher-order polymorphism¶

We should make it easy to write higher-order functions that behave
polymorphically with respect to whether their arguments throw. This
can be done in a fairly simple way: a function can declare that it
throws if any of a set of named arguments do. As an example (using
strawman syntax):

func map<T,U>(array: [T], fn: T -> U) throwsIf(fn) -> [U] {
  ...
}

There’s no need for a more complex logical operator than disjunction
for normal higher-order stuff.

This feature is highly desired (e.g. it would allow many otherwise
redundant overloads to be collapsed into a single definition), but it
may or may not make it into Swift 2.0 based on schedule limitations.

Generic polymorphism¶

For similar reasons to higher-order polymorphism, we should consider
making it easier to parameterize protocols on whether their operations
can throw. This would allow the writing of generic algorithms, e.g.
over Sequence, that handle both conformances that cannot throw (like
Array) and those that can (like a hypothetical cloud-backed
implementation).

However, this would be a very complex feature, yet to be designed, and
it is far out-of-scope for Swift 2.0. In the meantime, most standard
protocols will be written to not allow throwing conformances, so as to
not burden the use of common generic algorithms with spurious
error-handling code.

Statement-like functions¶

Some functions are designed to take trailing closures that feel like
sub-statements. For example, autoreleasepool can be used this way:

autoreleasepool {
  foo()
}

The error-handling model doesn’t cause major problems for this. The
compiler can infer that the closure throws, and autoreleasepool
can be overloaded on whether its argument closure throws; the
overload that takes a throwing closures would itself throw.

There is one minor usability problem here, though. If the closure
contains throwing expressions, those expression must be explicitly
marked within the closure with try. However, from the compiler’s
perspective, the call to autoreleasepool is also a call that
can throw, and so it must also be marked with try:

try autoreleasepool {    // 'try' is required here...
  let string = try parseString() // ...and here.
  ...
}

This marking feels redundant. We want functions like
autoreleasepool to feel like statements, but marks inside builtin
statements like if don’t require the outer statement to be marked.
It would be better if the compiler didn’t require the outer try.

On the other hand, the “statement-like” story already has a number of
other holes: for example, break, continue, and return
behave differently in the argument closure than in statements. In the
future, we may consider fixing that; that fix will also need to
address the error-propagation problem.

using

A using statement would acquire a resource, holds it for a fixed
period of time, optionally binds it to a name, and then releases it
whenever the controlled statement exits. using has many
similarities to defer. It does not subsume defer, which is useful
for many ad-hoc and tokenless clean-ups. But it could be convenient
for the common pattern of a type-directed clean-up.

Automatically importing CoreFoundation and C functions¶

CF APIs use CFErrorRef pretty reliably, but there are several
problems here: 1) the memory management rules for CFErrors are unclear
and potentially inconsistent. 2) we need to know when an error is
raised.

In principle, we could import POSIX functions into Swift as throwing
functions, filling in the error from errno. It’s nearly impossible
to imagine doing this with an automatic import rule, however; much
more likely, we’d need to wrap them all in an overlay.

In both cases, it is possible to pull these into the Swift error
handling model, but because this is likely to require massive SDK
annotations it is considered out of scope for iOS 9/OSX 10.11 & Swift 2.0.

Unexpected and universal errors¶

As discussed above, 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. For this reason, it is considered well out of scope for
Swift 2.0.

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!

Время прочтения
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. Легко и просто! И не нужно становиться профессиональным программистом.

Понравилась статья? Поделить с друзьями:
  • Error type card removed
  • Error type bigserial does not exist
  • Error type access violation
  • Error type 3 error activity class does not exist
  • Error type 0x01