Swift throw error

Error handling is the process of responding to and recovering from error conditions in your program. Swift provides first-class support for throwing, catching, propagating, and manipulating recoverable errors at runtime.

Error handling is the process of responding to and recovering from error conditions in your program. Swift provides first-class support for throwing, catching, propagating, and manipulating recoverable errors at runtime.

Some operations aren’t guaranteed to always complete execution or produce a useful output. Optionals are used to represent the absence of a value, but when an operation fails, it’s often useful to understand what caused the failure, so that your code can respond accordingly.

As an example, consider the task of reading and processing data from a file on disk. There are a number of ways this task can fail, including the file not existing at the specified path, the file not having read permissions, or the file not being encoded in a compatible format. Distinguishing among these different situations allows a program to resolve some errors and to communicate to the user any errors it can’t resolve.

Representing and Throwing Errors¶

In Swift, errors are represented by values of types that conform to the Error protocol. This empty protocol indicates that a type can be used for error handling.

Swift enumerations are particularly well suited to modeling a group of related error conditions, with associated values allowing for additional information about the nature of an error to be communicated. For example, here’s how you might represent the error conditions of operating a vending machine inside a game:

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

Throwing an error lets you indicate that something unexpected happened and the normal flow of execution can’t continue. You use a throw statement to throw an error. For example, the following code throws an error to indicate that five additional coins are needed by the vending machine:

  1. throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

Handling Errors¶

When an error is thrown, some surrounding piece of code must be responsible for handling the error—for example, by correcting the problem, trying an alternative approach, or informing the user of the failure.

There are four ways to handle errors in Swift. You can propagate the error from a function to the code that calls that function, handle the error using a docatch statement, handle the error as an optional value, or assert that the error will not occur. Each approach is described in a section below.

When a function throws an error, it changes the flow of your program, so it’s important that you can quickly identify places in your code that can throw errors. To identify these places in your code, write the try keyword—or the try? or try! variation—before a piece of code that calls a function, method, or initializer that can throw an error. These keywords are described in the sections below.

Note

Error handling in Swift resembles exception handling in other languages, with the use of the try, catch and throw keywords. Unlike exception handling in many languages—including Objective-C—error handling in Swift doesn’t involve unwinding the call stack, a process that can be computationally expensive. As such, the performance characteristics of a throw statement are comparable to those of a return statement.

Propagating Errors Using Throwing Functions¶

To indicate that a function, method, or initializer can throw an error, you write the throws keyword in the function’s declaration after its parameters. A function marked with throws is called a throwing function. If the function specifies a return type, you write the throws keyword before the return arrow (->).

  1. func canThrowErrors() throws -> String
  2. func cannotThrowErrors() -> String

A throwing function propagates errors that are thrown inside of it to the scope from which it’s called.

Note

Only throwing functions can propagate errors. Any errors thrown inside a nonthrowing function must be handled inside the function.

In the example below, the VendingMachine class has a vend(itemNamed:) method that throws an appropriate VendingMachineError if the requested item isn’t available, is out of stock, or has a cost that exceeds the current deposited amount:

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

The implementation of the vend(itemNamed:) method uses guard statements to exit the method early and throw appropriate errors if any of the requirements for purchasing a snack aren’t met. Because a throw statement immediately transfers program control, an item will be vended only if all of these requirements are met.

Because the vend(itemNamed:) method propagates any errors it throws, any code that calls this method must either handle the errors—using a docatch statement, try?, or try!—or continue to propagate them. For example, the buyFavoriteSnack(person:vendingMachine:) in the example below is also a throwing function, and any errors that the vend(itemNamed:) method throws will propagate up to the point where the buyFavoriteSnack(person:vendingMachine:) function is called.

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

In this example, the buyFavoriteSnack(person: vendingMachine:) function looks up a given person’s favorite snack and tries to buy it for them by calling the vend(itemNamed:) method. Because the vend(itemNamed:) method can throw an error, it’s called with the try keyword in front of it.

Throwing initializers can propagate errors in the same way as throwing functions. For example, the initializer for the PurchasedSnack structure in the listing below calls a throwing function as part of the initialization process, and it handles any errors that it encounters by propagating them to its caller.

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

Handling Errors Using Do-Catch¶

You use a docatch statement to handle errors by running a block of code. If an error is thrown by the code in the do clause, it’s matched against the catch clauses to determine which one of them can handle the error.

Here is the general form of a docatch statement:

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

You write a pattern after catch to indicate what errors that clause can handle. If a catch clause doesn’t have a pattern, the clause matches any error and binds the error to a local constant named error. For more information about pattern matching, see Patterns.

For example, the following code matches against all three cases of the VendingMachineError enumeration.

  1. var vendingMachine = VendingMachine()
  2. vendingMachine.coinsDeposited = 8
  3. do {
  4. try buyFavoriteSnack(person: «Alice», vendingMachine: vendingMachine)
  5. print(«Success! Yum.»)
  6. } catch VendingMachineError.invalidSelection {
  7. print(«Invalid Selection.»)
  8. } catch VendingMachineError.outOfStock {
  9. print(«Out of Stock.»)
  10. } catch VendingMachineError.insufficientFunds(let coinsNeeded) {
  11. print(«Insufficient funds. Please insert an additional (coinsNeeded) coins.»)
  12. } catch {
  13. print(«Unexpected error: (error))
  14. }
  15. // Prints «Insufficient funds. Please insert an additional 2 coins.»

In the above example, the buyFavoriteSnack(person:vendingMachine:) function is called in a try expression, because it can throw an error. If an error is thrown, execution immediately transfers to the catch clauses, which decide whether to allow propagation to continue. If no pattern is matched, the error gets caught by the final catch clause and is bound to a local error constant. If no error is thrown, the remaining statements in the do statement are executed.

The catch clauses don’t have to handle every possible error that the code in the do clause can throw. If none of the catch clauses handle the error, the error propagates to the surrounding scope. However, the propagated error must be handled by some surrounding scope. In a nonthrowing function, an enclosing docatch statement must handle the error. In a throwing function, either an enclosing docatch statement or the caller must handle the error. If the error propagates to the top-level scope without being handled, you’ll get a runtime error.

For example, the above example can be written so any error that isn’t a VendingMachineError is instead caught by the calling function:

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

In the nourish(with:) function, if vend(itemNamed:) throws an error that’s one of the cases of the VendingMachineError enumeration, nourish(with:) handles the error by printing a message. Otherwise, nourish(with:) propagates the error to its call site. The error is then caught by the general catch clause.

Another way to catch several related errors is to list them after catch, separated by commas. For example:

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

The eat(item:) function lists the vending machine errors to catch, and its error text corresponds to the items in that list. If any of the three listed errors are thrown, this catch clause handles them by printing a message. Any other errors are propagated to the surrounding scope, including any vending-machine errors that might be added later.

Converting Errors to Optional Values¶

You use try? to handle an error by converting it to an optional value. If an error is thrown while evaluating the try? expression, the value of the expression is nil. For example, in the following code x and y have the same value and behavior:

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

If someThrowingFunction() throws an error, the value of x and y is nil. Otherwise, the value of x and y is the value that the function returned. Note that x and y are an optional of whatever type someThrowingFunction() returns. Here the function returns an integer, so x and y are optional integers.

Using try? lets you write concise error handling code when you want to handle all errors in the same way. For example, the following code uses several approaches to fetch data, or returns nil if all of the approaches fail.

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

Disabling Error Propagation¶

Sometimes you know a throwing function or method won’t, in fact, throw an error at runtime. On those occasions, you can write try! before the expression to disable error propagation and wrap the call in a runtime assertion that no error will be thrown. If an error actually is thrown, you’ll get a runtime error.

For example, the following code uses a loadImage(atPath:) function, which loads the image resource at a given path or throws an error if the image can’t be loaded. In this case, because the image is shipped with the application, no error will be thrown at runtime, so it’s appropriate to disable error propagation.

  1. let photo = try! loadImage(atPath: «./Resources/John Appleseed.jpg»)

Specifying Cleanup Actions¶

You use a defer statement to execute a set of statements just before code execution leaves the current block of code. This statement lets you do any necessary cleanup that should be performed regardless of how execution leaves the current block of code—whether it leaves because an error was thrown or because of a statement such as return or break. For example, you can use a defer statement to ensure that file descriptors are closed and manually allocated memory is freed.

A defer statement defers execution until the current scope is exited. This statement consists of the defer keyword and the statements to be executed later. The deferred statements may not contain any code that would transfer control out of the statements, such as a break or a return statement, or by throwing an error. Deferred actions are executed in the reverse of the order that they’re written in your source code. That is, the code in the first defer statement executes last, the code in the second defer statement executes second to last, and so on. The last defer statement in source code order executes first.

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

The above example uses a defer statement to ensure that the open(_:) function has a corresponding call to close(_:).

Note

You can use a defer statement even when no error handling code is involved.

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

Обработка ошибок — это процесс реагирования на возникновение ошибок и восстановление после появления ошибок в программе. Swift предоставляет первоклассную поддержку при генерации, вылавливании и переносе ошибок, устранении ошибок во время выполнения программы.

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

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

Отображение и генерация ошибок

В Swift ошибки отображаются значениями типов, которые соответствуют протоколу Error. Этот пустой протокол является индикатором того, что это перечисление может быть использовано для обработки ошибок.

Перечисления в Swift особенно хорошо подходят для группировки схожих между собой условий возникновения ошибок и соответствующих им значений, что позволяет получить дополнительную информацию о природе самой ошибки. Например, вот как отображаются условия ошибки работы торгового автомата внутри игры:

enum VendingMachineError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
}

Генерация ошибки позволяет указать, что произошло что-то неожиданное и обычное выполнение программы не может продолжаться. Для того чтобы «сгенерировать» ошибку, вы используете инструкцию throw. Например, следующий код генерирует ошибку, указывая, что пять дополнительных монет нужны торговому автомату:

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

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

Когда генерируется ошибка, то фрагмент кода, окружающий ошибку, должен быть ответственным за ее обработку: например, он должен исправить ее, или испробовать альтернативный подход, или просто информировать пользователя о неудачном исполнении кода.

В Swift существует четыре способа обработки ошибок. Вы можете передать (propagate) ошибку из функции в код, который вызывает саму эту функцию, обработать ошибку, используя инструкцию do-catch, обработать ошибку, как значение опционала, или можно поставить утверждение, что ошибка в данном случае исключена. Каждый вариант будет рассмотрен далее.

Когда функция генерирует ошибку, последовательность выполнения вашей программы меняется, поэтому важно сразу обнаружить место в коде, которое может генерировать ошибки. Для того, чтобы выяснить где именно это происходит, напишите ключевое слово try — или варианты try? или try!— до куска кода, вызывающего функцию, метод или инициализатор, который может генерировать ошибку. Эти ключевые слова описываются в следующем параграфе.

Заметка

Обработка ошибок в Swift напоминает обработку исключений (exceptions) в других языках, с использованием ключевых слов try, catch и throw. В отличие от обработки исключений во многих языках, в том числе и в Objective-C- обработка ошибок в Swift не включает разворачивание стека вызовов, то есть процесса, который может быть дорогим в вычислительном отношении. Таким образом, производительные характеристики инструкции throw сопоставимы с характеристиками оператора return.

Передача ошибки с помощью генерирующей функции

Чтобы указать, что функция, метод или инициализатор могут генерировать ошибку, вам нужно написать ключевое слово throws в реализации функции после ее параметров. Функция, отмеченная throws называется генерирующей функцией. Если у функции установлен возвращаемый тип, то вы пишете ключевое слово throws перед стрелкой возврата (->).

func canThrowErrors() throws -> String
func cannotThrowErrors() -> String

Генерирующая функция передает ошибки, которые возникают внутри нее в область вызова этой функции.

Заметка

Только генерирующая ошибку функция может передавать ошибки. Любые ошибки, сгенерированные внутри non-throwing функции, должны быть обработаны внутри самой функции.

В приведенном ниже примере VendingMachine класс имеет vend(itemNamed: ) метод, который генерирует соответствующую VendingMachineError, если запрошенный элемент недоступен, его нет в наличии, или имеет стоимость, превышающую текущий депозит:

struct Item {
    var price: Int
    var count: Int
}
 
class VendingMachine {
    var inventory = [
        "Candy Bar": Item(price: 12, count: 7),
        "Chips": Item(price: 10, count: 4),
        "Pretzels": Item(price: 7, count: 11)
    ]
    var coinsDeposited = 0
    
    func vend(itemNamed name: String) throws {
        guard let item = inventory[name] else {
            throw VendingMachineError.invalidSelection
        }
        
        guard item.count > 0 else {
            throw VendingMachineError.outOfStock
        }
        
        guard item.price <= coinsDeposited else {
            throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
        }
        
        coinsDeposited -= item.price
        
        var newItem = item
        newItem.count -= 1
        inventory[name] = newItem
        
        print("Dispensing (name)")
    }
}

Реализация vend(itemNamed: ) метода использует оператор guard для раннего выхода из метода и генерации соответствующих ошибок, если какое-либо требование для приобретения закуски не будет выполнено. Потому что инструкция throw мгновенно изменяет управление программой, и выбранная позиция будет куплена, только если все эти требования будут выполнены.

Поскольку vend(itemNamed: ) метод передает все ошибки, которые он генерирует, вызывающему его коду, то они должны быть обработаны напрямую, используя оператор do-catch, try? или try!, или должны быть переданы дальше. Например, buyFavoriteSnack(person:vendingMachine: ) в примере ниже — это тоже генерирующая функция, и любые ошибки, которые генерирует метод vend(itemNamed: ), будут переноситься до точки, где будет вызываться функция buyFavoriteSnack(person:vendingMachine: ).

let favoriteSnacks = [
    "Alice": "Chips",
    "Bob": "Licorice",
    "Eve": "Pretzels"
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
    let snackName = favoriteSnacks[person] ?? "Candy Bar"
    try vendingMachine.vend(itemNamed: snackName)
}

В этом примере, функция buyFavoriteSnack(person:vendingMachine: ) подбирает любимые закуски данного человека и пытается их купить, вызывая vend(itemNamed: ) метод. Поскольку метод vend(itemNamed: ) может сгенерировать ошибку, он вызывается с ключевым словом try перед ним.

Генерирующие ошибку инициализаторы могут распространять ошибки таким же образом, как генерирующие ошибку функции. Например, инициализатор структуры PurchasedSnack в списке ниже вызывает генерирующую ошибку функции как часть процесса инициализации, и он обрабатывает любые ошибки, с которыми сталкивается, путем распространения их до вызывающего его объекта.

struct PurchasedSnack {
    let name: String
    init(name: String, vendingMachine: VendingMachine) throws {
        try vendingMachine.vend(itemNamed: name)
        self.name = name
    }
}

Обработка ошибок с использованием do-catch

Используйте инструкцию do-catch для обработки ошибок, запуская блок кода. Если выдается ошибка в коде условия do, она соотносится с условием catch для определения того, кто именно сможет обработать ошибку.

Вот общий вид условия do-catch:

do {
    try выражение
    выражение
} catch шаблон 1 {
    выражение
} catch шаблон 2  where условие {
    выражение
} catch шаблон 3, шаблон 4 where условие {
    выражение
} catch {
    выражение
}

Вы пишете шаблон после ключевого слова catch, чтобы указать какие ошибки могут обрабатываться данным пунктом этого обработчика. Если условие catch не имеет своего шаблона, то оно подходит под любые ошибки и связывает ошибки к локальной константе error. Более подробно о соответствии шаблону см. Шаблоны.

Например, следующий код обрабатывает все три случая в перечислении VendingMachineError, но все другие ошибки должны быть обработаны окружающей областью:

var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
    try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
} catch VendingMachineError.invalidSelection {
    print("Ошибка выбора.")
} catch VendingMachineError.outOfStock {
    print("Нет в наличии.")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
    print("Недостаточно средств. Пожалуйста вставьте еще (coinsNeeded) монетки.")
} catch {
    print("Неожиданная ошибка: (error).")
}
// Выведет "Недостаточно средств. Пожалуйста вставьте еще 2 монетки.

В приведенном выше примере, buyFavoriteSnack(person:vendingMachine: ) функция вызывается в выражении try, потому что она может сгенерировать ошибку. Если генерируется ошибка, выполнение немедленно переносится в условия catch, которые принимают решение о продолжении передачи ошибки. Если ошибка не генерируется, остальные операторы do выполняются.

В условии catch не нужно обрабатывать все возможные ошибки, которые может вызвать код в условии do. Если ни одно из условий catch не обрабатывает ошибку, ошибка распространяется на окружающую область. Однако распространяемая ошибка должна обрабатываться некоторой внешней областью. В функции nonthrowing условие включения do-catch должно обрабатывать ошибку. В функции throwing либо включающая условие do-catch, либо вызывающая сторона должна обрабатывать ошибку. Если ошибка распространяется на область верхнего уровня без обработки, вы получите ошибку исполнения.

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

func nourish(with item: String) throws {
    do {
        try vendingMachine.vend(itemNamed: item)
    } catch is VendingMachineError {
        print("Некорректный вывод, нет в наличии или недостаточно денег.")
    }
}

do {
    try nourish(with: "Beet-Flavored Chips")
} catch {
    print("Unexpected non-vending-machine-related error: (error)")
}
// Выведет "Некорректный вывод, нет в наличии или недостаточно денег."

В nourish(with: ), если vend(itemNamed : ) выдает ошибку, которая является одним из кейсов перечисления VendingMachineError, nourish(with: ) обрабатывает ошибку, печатая сообщение. В противном случае, nourish(with: ) распространяет ошибку на свое место вызова. Ошибка затем попадает в общее условие catch.

Преобразование ошибок в опциональные значения

Вы можете использовать try? для обработки ошибки, преобразовав ее в опциональное значение. Если ошибка генерируется при условии try?, то значение выражения вычисляется как nil. Например, в следующем коде x и y имеют одинаковые значения и поведение:

func someThrowingFunction() throws -> Int {
    // ...
}
 
let x = try? someThrowingFunction()
 
let y: Int?
do {
    y = try someThrowingFunction()
} catch {
    y = nil
}

Если someThrowingFunction() генерирует ошибку, значение x и y равно nil. В противном случае значение x и y — это возвращаемое значение функции. Обратите внимание, что x и y являются опциональными, независимо от того какой тип возвращает функция someThrowingFunction().

Использование try? позволяет написать краткий код обработки ошибок, если вы хотите обрабатывать все ошибки таким же образом. Например, следующий код использует несколько попыток для извлечения данных или возвращает nil, если попытки неудачные.

func fetchData() -> Data? {
    if let data = try? fetchDataFromDisk() { return data }
    if let data = try? fetchDataFromServer() { return data }
    return nil
}

Запрет на передачу ошибок

Иногда вы знаете, что функции throw или методы не сгенерируют ошибку во время исполнения. В этих случаях, вы можете написать try! перед выражением для запрета передачи ошибки и завернуть вызов в утверждение того, что ошибка точно не будет сгенерирована. Если ошибка на самом деле сгенерирована, вы получите сообщение об ошибке исполнения.

Например, следующий код использует loadImage(atPath: ) функцию, которая загружает ресурс изображения по заданному пути или генерирует ошибку, если изображение не может быть загружено. В этом случае, поскольку изображение идет вместе с приложением, сообщение об ошибке не будет сгенерировано во время выполнения, поэтому целесообразно отключить передачу ошибки.

let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")

Установка действий по очистке (Cleanup)

Вы используете оператор defer для выполнения набора инструкций перед тем как исполнение кода оставит текущий блок. Это позволяет сделать любую необходимую очистку, которая должна быть выполнена, независимо от того, как именно это произойдет — либо он покинет из-за сгенерированной ошибки или из-за оператора, такого как break или return. Например, вы можете использовать defer, чтобы удостовериться, что файл дескрипторов закрыт и выделенная память вручную освобождена.

Оператор defer откладывает выполнение, пока не происходит выход из текущей области. Этот оператор состоит из ключевого слова defer и выражений, которые должны быть выполнены позже. Отложенные выражения могут не содержать кода, изменяющего контроль исполнения изнутри наружу, при помощи таких операторов как break или return, или просто генерирующего ошибку. Отложенные действия выполняются в обратном порядке, как они указаны, то есть, код в первом операторе defer выполняется после кода второго, и так далее.

func processFile(filename: String) throws {
    if exists(filename) {
        let file = open(filename)
        defer {
            close(file)
        }
        while let line = try file.readline() {
            // работаем с файлом.
        }
        // close(file) вызывается здесь, в конце зоны видимости.
    }
}

Приведенный выше пример использует оператор defer, чтобы удостовериться, что функция open(_: ) имеет соответствующий вызов и для close(_: ).

Заметка

Вы можете использовать оператор defer, даже если не используете кода обработки ошибок.

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

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

Просмотры 9.7K

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

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

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

Моя история

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

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

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

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

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

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

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

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

var isInControl = true

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

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

Проблема

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

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

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

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

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

}

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

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


else { 
 slowDownTheCar()
 shutDownTheEngine()
}

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

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

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

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

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

enum TeslaError: Error {
 case lostGPS
 case lowBattery 
}

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

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

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

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

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

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

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

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


autoDriveTesla() // "HURRY! " 

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


func autoDriveTesla() throws { ... }

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

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

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

}
 if lowBattery {
  throw TeslaError.lowBattery
}

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

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

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

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

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

do {
 try autoDriveTesla() 
}

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

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

  1. TeslaError.lowBattery
  2. TeslaError.lostGPS.

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

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

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

// Results: "HURRY! "

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

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

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

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

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

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

try?– что это?

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

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

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

if let newCourse = newCourse { ... }

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

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

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

try! – что это?

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

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

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

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

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

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

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.

In Swift 2.0, Apple introduced the throws keyword in Swift. This addition to Swift language added the ability for developers to write code that clearly communicates that an error might occur during the execution of a code path, and it forces the caller of that code to handle, or explicitly ignore the error in-place. In this post I will show you what the throws keyword is exactly, and how you can deal with errors in your codebase.

Working with code that throws errors

If you’ve worked with JSONDecoder in Swift, you have already experienced code that can throw an error. Let’s look at a quick example to refresh your mind.

do {
  let data = "hello, world".data(using: .utf8)!
  let decoder = JSONDecoder()
  let string = try decoder.decode(String.self, from: data)
} catch { error in
  print("something went wrong!")
  print(error)
}

This code is a very basic example of how you can work with code that might throw an error. When you call a function that can throw an error, you must prefix this invocation with the try keyword. You can’t just do this anywhere. Calling out to code that might throw an error must occur in a do {} catch {} block. The try prefixed code must be in the do portion of the block. You can have more than one try prefixed method call in a single do block. When any of those method calls throws an error, execution is immediately moved to the catch part.

The catch block receives a single argument, which is the Error that was thrown in your do block. In Swift, Error is a protocol that is used to represent errors. It’s possible to specialize your catch block to make it catch only a specific kind of error. If you do this, you still need a general purpose catch that will catch all other Error types. Let’s look at an example:

do {
  let data = "hello, world".data(using: .utf8)!
  let decoder = JSONDecoder()
  let string = try decoder.decode(String.self, from: data)
} catch is DecodingError {
  print("something went wrong while decoding!")
} catch { error
  print("something went wrong!")
  print(error)
}

You can use pattern matching to specialize catch clauses for an entire category of errors, or a specific error (catch is MyError.someCase). Doing this can be convenient if you have special handling paths for certain errors, but you do lose access to the original error. The final catch-all is invoked if none of the specialized catch blocks match the thrown error.

There are cases when you might make the decision that you don’t want to handle any thrown errors. In these cases, you can use the try? prefix instead of try. When you call a throwing function with try?, the function’s return type becomes an Optional that’s nil when an error was thrown. Let’s look at an example:

enum MyError: Error {
  case myErrorCase
}

func doSomething() throws {
  throw MyError.myErrorCase
}

print(try? doSomething())

This example is pretty worthless in a real codebase, but it illustrates my point nicely. The doSomething() function doesn’t return anything. This means that it returns Void, which can also be written as an empty tuple (()). The printed result of this code is nil. If you comment out the throw line from doSomething(), the printed output is Optiona(()). In other words, an optional with Void as its value. If a function has a return value and you call it with try?, the result is also optional. Let’s look at another example:

let data = "hello, world".data(using: .utf8)!
let decoder = JSONDecoder()
let string = try? decoder.decode(String.self, from: data)

print(string)

If you run this code in a Playground, you’ll find that the printed value is nil. The provided data isn’t valid JSON, so it fails to decode. But because we call decoder.decode(_:from:) with try?, the error is hidden and decode(_:from:) returns an optional value instead of its normal non-optional value.

If you call a throwing function that returns an optional with try?, you might expect a nested optional to be returned. After all, the function itself returns an optional and the return type of a throwing function is wrapped by an optional when you call it with try?. This means that a function that returns String returns String? when you call it with try?. However, if you call a function that returns String?, it doesn’t return String??. Swift will automatically work around this nested optional, which is both a blessing and a curse. Consider the following code:

let string = try? returnsOptionalString()

if string == nil {
  // why is string nil? Is it due to an error? Or did the function execute successfully and we just got back nil without encountering any errors?
}

While the code above might be more convenient to write than a do {} catch {} block, you lose all error-related information. And in cases where code returns an optional, you don’t whether you received nil because of an error. You should only use try? if you truly don’t care about handling errors, or knowing whether an error occurred at all.

There is one more way to deal with code that can throw errors. You can use try! in cases where you’re absolutely sure that your code shouldn’t throw an error, and you want your app to crash if it does. This flavor of try should be used sparingly, and preferably not at all. Let’s look at one last example:

enum MyError: Error {
  case myErrorCase
}

func doSomething() throws {
  throw MyError.myErrorCase
}

try! doSomething() // 

This code would crash at runtime. doSomething() always throws an error, and by calling it with the try! prefix we tell Swift that we don’t expect doSomething() to actually throw an error. And when it does, execution of the program should halt, and the app should crash. This is quite radical and, again, should be used sparingly.

Throwing errors in your own code

Sometimes, the code you write needs a way to express that something went wrong and execution of that code path needs to stop immediately with an error. If the error is recoverable, you might have a good candidate for a throwing function on your hands. When I say that an error is recoverable, I mean that the error didn’t occur due to a programming error (like accessing an out of bounds index in an array for example) and that the program isn’t in an unexpected or invalid state. It might simply mean that something went wrong.

For example, when you try to decode invalid JSON using a JSONDecoder, that’s not considered an error that’s severe enough to crash the app. Instead, an error is thrown to let you know that something went wrong. This is an important distinction, and trying to decode invalid JSON should never crash the application. At least not in the JSONDecoder. You’re free to crash your app is a decoding error occurs if you want but I’d strongly advise you not to. Especially if you’re loading JSON from a webserver.

When you’re writing your own code, you might want to throw an error of your own. You already saw how to do this in the previous section in the doSomething function:

func doSomething() throws {
  throw MyError.myErrorCase
}

Functions that can throw an error must have the throws keyword appended to their signature, before the return type. Here’s what a function signature for a throwing function with a return type looks like:

func returnsOptionalString() throws -> String? {
  // do work
}

When you’re writing code in a so-called throwing function, you can call methods that throw errors without using a do {} catch {} block:

func decodeJSON(_ data: Data) throws -> String {
  let decoder = JSONDecoder()
  let decodedString = try decoder.decode(String.self, from: data)

  return decodedString
}

This code is okay because Swift knows that decodeJSON(_:) might encounter and throw an error. When the JSON decoding fails in this function, the thrown error is passed up to the called of decodeJSON because it’s marked as throwing with the throws keyword. When this function is called from another function that’s throwing, that function will also forward the error up to its caller. The error will be passed up all the way to a caller that’s not part of a throwing function.

There is one more error throwing related keyword that I want to show you. It’s called rethrows. The rethrows keyword is used for functions that don’t directly throw an error. Instead, the functions take a closure argument where the closure might throw instead of the function itself. Let’s look at an example of a function that takes a throwing closure without rethrows:

func execute(_ closure: (() throws -> Void)) throws {
  try closure()
}

do {
  try execute {
    print("hello!")
  }

  try execute { 
    throw MyError.myErrorCase
  }
} catch {
  print(error)
}

In the code above I have defined an execute function. This function takes a single closure argument, and all it does is execute the closure immediately. Nothing fancy. You’ll see that both execute(_:) and the closure it receives are marked with throws. It’s important to understand that marking a function as throwing does not mean that the function is guaranteed to throw an error. All we’re saying is that it might. This is especially relevant for the closure argument. The closure that’s passed might not even be capable of throwing an error, just like the first call to execute(_:) in this example. Even though we know that this closure never throws an error, and the compiler also knows it, we must mark the call to execute(_:) with try because that function itself might throw an error.

We can clean this code up a little bit by declaring execute(_:) as rethrowing rather than throwing:

func execute(_ closure: (() throws -> Void)) rethrows {
  try closure()
}

execute {
  print("hello!")
}

do {
  try execute {
    throw MyError.myErrorCase
  }
} catch {
  print(error)
}

Because execute(_:) is now rethrowing, the Swift compiler can verify whether a code path might throw an error, and it will allow you to call execute(_:) without try if it can prove that the closure you passed to execute(_:) doesn’t throw an error. Quite neat right?

You’ll find rethrows in several places in the Swift standard library. For example, map(_:) is marked with rethrows because the closure you supply to map(_:) is allowed to throw errors if needed:

let mapped: [Int] = try [1, 2, 3].map { int in
  if int > 3 {
    throw MyError.intLargerThanThree
  }

  return int * 2
}

This probably isn’t how you commonly use map(_:) because typically the closure passed to this function doesn’t throw. But now you know that you’re allowed to throw errors while mapping, and you also know why you’re not forced to mark every call to map(_:) with try.

Note that the execution of any throwing method or closure is halted immediately when an error is thrown:

func doSomething() throws {
  throw MyError.myErrorCase

  print("This is never printed")
}

Throwing an error is a strong signal that something’s wrong, and there’s no point in fully executing the current code path. The error is sent to the caller of the throwing function, and it must be handled or forwarded from there. If you throw an error in a function that should return something, the function will not actually return anything. Instead, your code switches to the catch part of the do {} catch {} block immediately where you can handle the error. The exception here is when the called of your throwing function calls it with try? or try! like I explained in the previous section.

In Summary

In this post, I’ve shown you how you can deal with functions that can throw errors in Swift. You saw how you can call functions that are marked as throwing, how you can tell Swift you’re not interested in handling an error and how you can tell Swift that you’re absolutely sure a certain call will never actually throw an error at runtime.

After that, I moved on to show you how you can throw errors from your own code, what the rethrows keyword is and when it’s useful.

If you have any feedback for me about this post, or if you have any questions, don’t hesitate to reach out to me on Twitter.

Try catch in Swift combined with throwing errors make it possible to nicely handle any failures in your code. A method can be defined as throwing which basically means that if anything goes wrong, it can throw an error. To catch this error, we need to implement a so-called do-catch statement.

It’s not always required to use a do-catch statement with throwing method. Let’s go over all the cases and cover them in more detail.

Learn about the 3-step process to transition your iOS development to Apple M1 silicon Macs — with insights from Reddit’s successful M1 transition, FAQs, and helpful M1 transition tips.

Creating a throwing method using the throws keyword

Creating a throwing method is as easy as adding the throws keyword to a method just before the return statement. In this example, we use a method to update the name for a user of a specific user identifier.

func update(name: String, forUserIdentifier userIdentifier: String) {
    // This method is not throwing any errors
}

func update(name: String, forUserIdentifier userIdentifier: String) throws {
    // The throws keyword makes that this method cán throw an error
}

When calling a throwing method you might get the following error:

Call can throw but is not marked with ‘try’

This means that you have to use the try keyword before a piece of code that can throw an error.

try update(name: "Antoine van der Lee", forUserIdentifier: "AEDKM1323")

Throwing initializer in Swift

A great thing is that you can also create a throwing initializer. This especially comes in handy when you want to validate properties for initializing a certain object. For example, you might want to validate a username before creating a User object.

struct User {
    enum ValidationError: Error {
        case emptyName
        case nameToShort(nameLength: Int)
    }

    let name: String

    init(name: String) throws {
        guard !name.isEmpty else {
            throw ValidationError.emptyName
        }
        guard name.count > 2 else {
            throw ValidationError.nameToShort(nameLength: name.count)
        }

        self.name = name
    }
}

let user = try User(name: "Antoine van der Lee")

Swift Try Catch: Handling Errors in Swift with a do-catch statement

To catch a thrown error in Swift we need to use the do-catch statement. The following example uses the earlier defined User instance.

do {
    let user = try User(name: "")
    print("Created user with name (user.name)")
} catch {
    print("User creation failed with error: (error)")
}

// Prints: User creation failed with error: emptyName

The emptyName error is thrown as the provided username is empty. The result is that the catch block is called. As you can see we can use a local error property to print out the caught error. The catch block is only called when an error occurs.

Catching a specific type of error

As we can’t specify the error type which will be thrown by a method we have to take into account that different kinds of error types can be thrown. Therefore, you want to catch and handle specific error types in a separate catch statement.

In the following example, we have implemented the name update method. This method can now throw both a user validation error and a database error thrown from the fetchUser method.

func fetchUser(for identifier: String) throws -> User {
    // Fetches the user from the database
}

func update(name: String, forUserIdentifier userIdentifier: String) throws {
    guard !name.isEmpty else {
        throw User.ValidationError.emptyName
    }
    var user = try fetchUser(for: userIdentifier)
    user.update(name)
    user.save()
}

It would be nice to catch the errors in separated blocks to display a different alert if only the name is invalid. There are several ways of doing this:

do {
    try update(name: "Antoine van der Lee", forUserIdentifier: "AEDKM1323")
} catch User.ValidationError.emptyName {
    // Called only when the `User.ValidationError.emptyName` error is thrown
} catch User.ValidationError.nameToShort(let nameLength) where nameLength == 1 {
    // Only when the `nameToShort` error is thrown for an input of 1 character
} catch is User.ValidationError {
    // All `User.ValidationError` types except for the earlier catch `emptyName` error.
} catch {
    // All other errors
}

There are a few things to point out here:

  • The order of catching is important. In this example, we first catch emptyName specific, all other User.ValidationError after that. If we would swap these two, the specific emptyName catch would never be called.
  • where can be used to filter down on error values. In this example, we only like to catch name inputs with a length of 1 character specifically. If you’re not familiar with the “where” keyword, you can check out my blog post Where usage in Swift.
  • Using the is keyword we can catch errors of a specific type.
  • The generic catch closure in the end catches all other errors.

There’s also scenarios in which you’d like to catch two or more specific error types. In this case, you can use lists in your catch statements:

do {
    try update(name: "Antoine van der Lee", forUserIdentifier: "AEDKM1323")
} catch User.ValidationError.emptyName, User.ValidationError.nameToShort {
    // Only called for `emptyName` and `nameToShort`
} catch {
    // All other errors
}

Note here that we took away the nameToShort paramater. This is something you can always do when working with enums if you’re not interested in the associated value.

Using try? with a throwing method

If you’re not interested in catching any thrown errors you can also decide to use try?. The question mark behind the try keyword basically tells that we’re not interested in the possibly thrown error.

let user = try? User(name: "")
print(user?.name) // Prints "nil" if an error occurred upon init.

The value will either be an optional User instance or nil and the thrown error is completely ignored.

Using try! with a throwing method

If you want your app to fail instantly you can use try! with the throwing method. This will basically fail your app just like a fatal error statement.

let user = try! User(name: "")
print(user.name)

This will end up with the following error as the name input is empty:

Fatal error: ‘try!’ expression unexpectedly raised an error: User.ValidationError.emptyName

Learn about the 3-step process to transition your iOS development to Apple M1 silicon Macs — with insights from Reddit’s successful M1 transition, FAQs, and helpful M1 transition tips.

Conclusion

Error handling in Swift is great. It allows you to write readable code while also handling the non-happy flow. By being able to catch specific error types or using the where keyword we have the flexibility to handle specific scenarios if needed. Be a good citizen and make your methods throw whenever it can fail!

If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!

Paul Hudson    September 23rd 2019    @twostraws

Swift works hard to make sure we can write safe software, which means it eliminates many opportunities for our code to fail. One of the ways it accomplishes this is by letting us catch errors when risky code doesn’t run according to plan, and in this article I’m going to walk through how that works and how to use it in your own code.

The Swift approach: try, catch, do and throw

If we wanted to load a file from a URL into a `Data` instance, we might write code like this:

let contents: Data?
do {
    contents = try Data(contentsOf: someURL)
} catch {
    contents = nil
}

That illustrates three of the five new keywords you’ll need to learn.

The fourth and fifth keywords are throw and throws, and we’ll look at them in depth now.

Please create a new Xcode project, using the Single View App template. You can name it whatever you feel like, and target whatever device you want – it doesn’t matter, because we’re not doing anything visual here.

Select ViewController.swift and add this new method:

func encrypt(_ str: String, withPassword password: String) -> String {
    // complicated encryption goes here
    let encrypted = password + str + password
    return String(encrypted.reversed())
}

That method is going to encrypt an string using the password that gets sent in. Well, it’s not actually going to do that – this article isn’t about encryption, so my «encryption» algorithm is pathetic: it puts the password before and after the input string, then reverses it. You’re welcome to add the complex encryption algorithm yourself later on!

Modify viewDidLoad() to call that method by adding this:

let encrypted = encrypt("secret information!", withPassword: "12345")
print(encrypted)

When you run your app now, you’ll see «54321!noitamrofni terces54321» printed out in the Xcode terminal. Easy, right?

But there’s a problem: assuming you actually do put in a meaningful encryption algorithm, there’s nothing stopping users from entering an empty string for a password, entering obvious passwords such as «password», or even trying to call the encryption method without any data to encrypt!

Swift comes to the rescue: you can tell Swift that this method can throw an error if it finds itself in an unacceptable state, such as if the password is six or fewer characters. Those errors are defined by you, and Swift goes some way to ensuring you catch them all.

To get started, we need the throws keyword, which you add to your method definition before its return value, like this:

func encrypt(_ str: String, withPassword password: String) throws -> String {
    // complicated encryption goes here
    let encrypted = password + str + password
    return String(encrypted.reversed())
}

As soon as you do that, your code stops working: adding throws has actually made things worse! But it’s worse for a good reason: Swift’s try/catch system is designed to be clear to developers, which means you need to mark any methods that can throw using the try keyword, like this:

let encrypted = try encrypt("secret information!", withPassword: "12345")

…but even now your code won’t compile, because you haven’t told Swift what to do when an error is thrown. This is where the do and catch keywords come in: they start a block of code that might fail, and handle those failures. In our basic example, it might look like this:

do {
    let encrypted = try encrypt("secret information!", withPassword: "12345")
    print(encrypted)
} catch {
    print("Something went wrong!")
}

That silences all the errors, and your code runs again. But it’s not actually doing anything interesting yet, because even though we say encrypt() has the potential to throw an error, it never actually does.

How to throw an error in Swift

Before you can throw an error, you need to make a list of all the possible errors you want to throw. In our case, we’re going to stop people from providing empty passwords, short passwords and obvious passwords, but you can extend it later.

To do this, we need to create an enum that represents our type of error. This needs to build on the built-in Error enum, but otherwise it’s easy. Add this before class ViewController:

enum EncryptionError: Error {
    case empty
    case short
}

That defines our first two encryption error types, and we can start using them immediately. As these are preconditions to running the method, we’re going to use the new guard keyword to make our intentions clear.

Put this at the start of encrypt():

guard password.count > 0 else { throw EncryptionError.empty }
guard password.count >= 5 else { throw EncryptionError.short }

If you run the app now nothing will have changed, because we’re providing the password «12345». But if you set that to an empty string, you’ll see «Something went wrong!» printed in the Xcode console, showing the error.

Of course, having a single error message isn’t helpful – there are several ways the method call can fail, and we want to provide something meaningful for each of them. So, modify the try/catch block in viewDidLoad() to this:

do {
    let encrypted = try encrypt("secret information!", withPassword: "")
    print(encrypted)
} catch EncryptionError.empty {
    print("You must provide a password.")
} catch EncryptionError.short {
    print("Passwords must be at least five characters, preferably eight or more.")
} catch {
    print("Something went wrong!")
}

Now there are meaningful error messages, so our code is starting to look better. But you may notice that we still need a third catch block in there even though we already caught both the .empty and .ehort cases.

Swift wants exhaustive try/catch error handling

If you recall, I said «Swift goes some way to ensuring you catch them all» and here’s where that becomes clear: we’re catching both errors we defined, but Swift also wants us to define a generic catch all to handle any other errors that might occur. We don’t tell Swift what kind of error our encryption method might throw, just that it throws something, so this extra catch-all block is required.

This does have one downside: if you add any future values to the enum, which we’re about to do, it will just drop into the default catch block – you won’t be asked to provide any code for it as would happen with a switch/case block.

We’re going to add a new value to our enum now, to detect obvious passwords. But we’re going to use Swift’s super-powerful enums so that we can return a message along with the error type. So, modify the EncryptionError enum to this:

enum EncryptionError: Error {
    case empty
    case short
    case obvious(String)
}

Now when you want to throw an error of type EncryptionError.obvious you must provide a reason.

guard password != "12345" else { throw EncryptionError.obvious("I've got the same passcode on my luggage!") }

Obviously you don’t want to provide hundreds (or thousands!) of guard statements to filter out obvious passwords, but hopefully you remember how to use UITextChecker to do spell checking – that would be a smart thing here!

That’s our basic do/try/throw/catch Swift example complete. You might look at the try statement and think it useless, but it’s primarily there to signal to developers «this call might fail.» This matters: when a try calls fails, execution immediately jumps to the catch blocks, so if you see try before a call it signals that the code beneath it might not get called.

There’s one more thing to discuss, which is what to do if you know a call simply can’t fail, for whatever reason. Now, clearly this is a decision you need to make on a case-by-case basic, but if you know there’s absolutely no way a method call might fail, or if it did fail then your code was so fundamentally broken that you might as well crash, you can use try! to signal this to Swift.

When you use the try! keyword, you don’t need to have do and catch around your code, because you’re promising it won’t ever fail. Instead, you can just write this:

let encrypted = try! encrypt("secret information!", withPassword: "12345")
print(encrypted)

Using the try! keyword communicates your intent clearly: you’re aware there’s the theoretical possibility of the call failing, but you’re certain it won’t happen in your use case. For example, if you’re trying to load the contents of a file in your app’s bundle, any failure effectively means your app bundle is damaged or unavailable, so you should terminate.

That’s all for error handling in Swift. If you’d like to learn about how Swift handles try/finally you should read my article on Swift’s defer keyword.

Понравилась статья? Поделить с друзьями:
  • Swift network error
  • Swift init error
  • Swift error localizeddescription
  • Swift error code z117
  • Swift decoding error