Swift error class

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.

What I am trying to achieve is perform a URLSession request in swift 3. I am performing this action in a separate function (so as not to write the code separately for GET and POST) and returning the URLSessionDataTask and handling the success and failure in closures. Sort of like this-

let task = URLSession.shared.dataTask(with: request) { (data, uRLResponse, responseError) in

     DispatchQueue.main.async {

          var httpResponse = uRLResponse as! HTTPURLResponse

          if responseError != nil && httpResponse.statusCode == 200{

               successHandler(data!)

          }else{

               if(responseError == nil){
                     //Trying to achieve something like below 2 lines
                     //Following line throws an error soo its not possible
                     //var errorTemp = Error(domain:"", code:httpResponse.statusCode, userInfo:nil)

                     //failureHandler(errorTemp)

               }else{

                     failureHandler(responseError!)
               }
          }
     }
}

I do not wish to handle the error condition in this function and wish to generate an error using the response code and return this Error to handle it wherever this function is called from.
Can anybody tell me how to go about this? Or is this not the «Swift» way to go about handling such situations?

asked Nov 18, 2016 at 7:59

Rikh's user avatar

RikhRikh

4,0183 gold badges14 silver badges34 bronze badges

4

In your case, the error is that you’re trying to generate an Error instance. Error in Swift 3 is a protocol that can be used to define a custom error. This feature is especially for pure Swift applications to run on different OS.

In iOS development the NSError class is still available and it conforms to Error protocol.

So, if your purpose is only to propagate this error code, you can easily replace

var errorTemp = Error(domain:"", code:httpResponse.statusCode, userInfo:nil)

with

var errorTemp = NSError(domain:"", code:httpResponse.statusCode, userInfo:nil)

Otherwise check the Sandeep Bhandari‘s answer regarding how to create a custom error type

Nikhil Manapure's user avatar

answered Nov 18, 2016 at 8:16

Luca D'Alberti's user avatar

Luca D’AlbertiLuca D’Alberti

4,7093 gold badges29 silver badges43 bronze badges

5

You can create a protocol, conforming to the Swift LocalizedError protocol, with these values:

protocol OurErrorProtocol: LocalizedError {

    var title: String? { get }
    var code: Int { get }
}

This then enables us to create concrete errors like so:

struct CustomError: OurErrorProtocol {

    var title: String?
    var code: Int
    var errorDescription: String? { return _description }
    var failureReason: String? { return _description }

    private var _description: String

    init(title: String?, description: String, code: Int) {
        self.title = title ?? "Error"
        self._description = description
        self.code = code
    }
}

answered Mar 7, 2017 at 18:01

Harry Bloom's user avatar

Harry BloomHarry Bloom

2,31924 silver badges17 bronze badges

2

You should use NSError object.

let error = NSError(domain: "", code: 401, userInfo: [ NSLocalizedDescriptionKey: "Invalid access token"])

Then cast NSError to Error object.

Saeed All Gharaee's user avatar

answered Feb 18, 2018 at 15:41

Ahmed Lotfy's user avatar

Ahmed LotfyAhmed Lotfy

3,70625 silver badges28 bronze badges

You can create enums to deal with errors :)

enum RikhError: Error {
    case unknownError
    case connectionError
    case invalidCredentials
    case invalidRequest
    case notFound
    case invalidResponse
    case serverError
    case serverUnavailable
    case timeOut
    case unsuppotedURL
 }

and then create a method inside enum to receive the http response code and return the corresponding error in return :)

static func checkErrorCode(_ errorCode: Int) -> RikhError {
        switch errorCode {
        case 400:
            return .invalidRequest
        case 401:
            return .invalidCredentials
        case 404:
            return .notFound
        //bla bla bla
        default:
            return .unknownError
        }
    }

Finally update your failure block to accept single parameter of type RikhError :)

I have a detailed tutorial on how to restructure traditional Objective — C based Object Oriented network model to modern Protocol Oriented model using Swift3 here https://learnwithmehere.blogspot.in Have a look :)

Hope it helps :)

answered Nov 18, 2016 at 8:08

Sandeep Bhandari's user avatar

Sandeep BhandariSandeep Bhandari

19.7k5 gold badges42 silver badges75 bronze badges

3

Details

  • Xcode Version 10.2.1 (10E1001)
  • Swift 5

Solution of organizing errors in an app

import Foundation

enum AppError {
    case network(type: Enums.NetworkError)
    case file(type: Enums.FileError)
    case custom(errorDescription: String?)

    class Enums { }
}

extension AppError: LocalizedError {
    var errorDescription: String? {
        switch self {
            case .network(let type): return type.localizedDescription
            case .file(let type): return type.localizedDescription
            case .custom(let errorDescription): return errorDescription
        }
    }
}

// MARK: - Network Errors

extension AppError.Enums {
    enum NetworkError {
        case parsing
        case notFound
        case custom(errorCode: Int?, errorDescription: String?)
    }
}

extension AppError.Enums.NetworkError: LocalizedError {
    var errorDescription: String? {
        switch self {
            case .parsing: return "Parsing error"
            case .notFound: return "URL Not Found"
            case .custom(_, let errorDescription): return errorDescription
        }
    }

    var errorCode: Int? {
        switch self {
            case .parsing: return nil
            case .notFound: return 404
            case .custom(let errorCode, _): return errorCode
        }
    }
}

// MARK: - FIle Errors

extension AppError.Enums {
    enum FileError {
        case read(path: String)
        case write(path: String, value: Any)
        case custom(errorDescription: String?)
    }
}

extension AppError.Enums.FileError: LocalizedError {
    var errorDescription: String? {
        switch self {
            case .read(let path): return "Could not read file from "(path)""
            case .write(let path, let value): return "Could not write value "(value)" file from "(path)""
            case .custom(let errorDescription): return errorDescription
        }
    }
}

Usage

//let err: Error = NSError(domain:"", code: 401, userInfo: [NSLocalizedDescriptionKey: "Invaild UserName or Password"])
let err: Error = AppError.network(type: .custom(errorCode: 400, errorDescription: "Bad request"))

switch err {
    case is AppError:
        switch err as! AppError {
        case .network(let type): print("Network ERROR: code (type.errorCode), description: (type.localizedDescription)")
        case .file(let type):
            switch type {
                case .read: print("FILE Reading ERROR")
                case .write: print("FILE Writing ERROR")
                case .custom: print("FILE ERROR")
            }
        case .custom: print("Custom ERROR")
    }
    default: print(err)
}

answered Jul 17, 2019 at 17:24

Vasily  Bodnarchuk's user avatar

Vasily BodnarchukVasily Bodnarchuk

23.7k9 gold badges130 silver badges127 bronze badges

Implement LocalizedError:

struct StringError : LocalizedError
{
    var errorDescription: String? { return mMsg }
    var failureReason: String? { return mMsg }
    var recoverySuggestion: String? { return "" }
    var helpAnchor: String? { return "" }

    private var mMsg : String

    init(_ description: String)
    {
        mMsg = description
    }
}

Note that simply implementing Error, for instance, as described in one of the answers, will fail (at least in Swift 3), and calling localizedDescription will result in the string «The operation could not be completed. (.StringError error 1.)»

answered Jun 30, 2017 at 20:10

prewett's user avatar

prewettprewett

1,55714 silver badges19 bronze badges

3

I still think that Harry’s answer is the simplest and completed but if you need something even simpler, then use:

struct AppError {
    let message: String

    init(message: String) {
        self.message = message
    }
}

extension AppError: LocalizedError {
    var errorDescription: String? { return message }
//    var failureReason: String? { get }
//    var recoverySuggestion: String? { get }
//    var helpAnchor: String? { get }
}

And use or test it like this:

printError(error: AppError(message: "My App Error!!!"))

func print(error: Error) {
    print("We have an ERROR: ", error.localizedDescription)
}

answered Jul 7, 2020 at 8:20

Reimond Hill's user avatar

Reimond HillReimond Hill

3,92932 silver badges48 bronze badges

 let error = NSError(domain:"", code:401, userInfo:[ NSLocalizedDescriptionKey: "Invaild UserName or Password"]) as Error
            self.showLoginError(error)

create an NSError object and typecast it to Error ,show it anywhere

private func showLoginError(_ error: Error?) {
    if let errorObj = error {
        UIAlertController.alert("Login Error", message: errorObj.localizedDescription).action("OK").presentOn(self)
    }
}

answered Apr 22, 2019 at 15:59

Suraj K Thomas's user avatar

Suraj K ThomasSuraj K Thomas

5,6854 gold badges51 silver badges64 bronze badges

protocol CustomError : Error {

    var localizedTitle: String
    var localizedDescription: String

}

enum RequestError : Int, CustomError {

    case badRequest         = 400
    case loginFailed        = 401
    case userDisabled       = 403
    case notFound           = 404
    case methodNotAllowed   = 405
    case serverError        = 500
    case noConnection       = -1009
    case timeOutError       = -1001

}

func anything(errorCode: Int) -> CustomError? {

      return RequestError(rawValue: errorCode)
}

answered Feb 9, 2019 at 15:10

Daniel.scheibe's user avatar

I know you have already satisfied with an answer but if you are interested to know the right approach, then this might be helpful for you.
I would prefer not to mix http-response error code with the error code in the error object (confused? please continue reading a bit…).

The http response codes are standard error codes about a http response defining generic situations when response is received and varies from 1xx to 5xx ( e.g 200 OK, 408 Request timed out,504 Gateway timeout etc — http://www.restapitutorial.com/httpstatuscodes.html )

The error code in a NSError object provides very specific identification to the kind of error the object describes for a particular domain of application/product/software. For example your application may use 1000 for «Sorry, You can’t update this record more than once in a day» or say 1001 for «You need manager role to access this resource»… which are specific to your domain/application logic.

For a very small application, sometimes these two concepts are merged. But they are completely different as you can see and very important & helpful to design and work with large software.

So, there can be two techniques to handle the code in better way:

1. The completion callback will perform all the checks

completionHandler(data, httpResponse, responseError) 

2. Your method decides success and error situation and then invokes corresponding callback

if nil == responseError { 
   successCallback(data)
} else {
   failureCallback(data, responseError) // failure can have data also for standard REST request/response APIs
}

Happy coding :)

answered Nov 18, 2016 at 9:07

Tushar's user avatar

TusharTushar

3,0122 gold badges25 silver badges26 bronze badges

1

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

Recoverable vs. unrecoverable errors

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

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

Sources of errors

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

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

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

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

Handling errors

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

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

1. User experience considerations

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

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

2. Where to put error handling code

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

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

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

Using optionals for simple error handling

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

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

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

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

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

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

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

Here, we demonstrate it with an array.

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

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

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

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

Representing errors

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

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

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

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

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

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

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

Throwing errors

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

throw EnrollmentError.courseFull

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

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

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

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

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

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

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

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

Note the keyword throws in the signature of the method.

Propagating errors

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

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

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

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

Catching errors

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

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

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

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

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

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

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

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

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

Here is some code to test the enrollment process.

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

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

Selectively propagating errors

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

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

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

Let’s see how this works.

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

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

Try vs. try? vs. try!

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

1. Converting errors to optionals with try?

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

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

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

enum TestError: Error {
    case someError
}

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

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

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

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

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

2. Asserting that errors will not occur with try!

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

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

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

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

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

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

Throws vs. rethrows

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

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

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

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

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

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

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

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

Now we can test the three scenarios.

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

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

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

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

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

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

Performing clean-up actions with defer

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

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

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

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

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

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

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

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

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

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

Errors and polymorphism

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

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

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

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

protocol X {
    func someFunc()
}

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

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

protocol Y {
    func anotherFunc() throws
}

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

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

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

Conclusion

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

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

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

Понравилась статья? Поделить с друзьями:
  • Sweet home 3d ошибка драйвер directx opengl
  • Putty connection fatal error network error connection refused
  • Systematic error manhwa
  • Swap ink swap error
  • Systematic error manga