Creating custom error types is an important skill when designing clear APIs for iOS and macOS applications. This post presents examples for creating, extending, throwing, and catching custom error types in Swift:
- Create A Custom Error And Conform To The Error Type
- Extend A Custom Error
- description for Custom Errors Using CustomStringConvertible
- localizedDescription For Custom Errors Using LocalizedError
- Throw Custom Errors
- Catch Custom Errors
Create A Custom Error And Conform To The Error Type
To create a custom error, create an enum
in Swift that conforms to the Error
protocol. Each case of the enum represents a unique error that can be thrown and handled:
enum CustomError: Error {
// Throw when an invalid password is entered
case invalidPassword
// Throw when an expected resource is not found
case notFound
// Throw in all other cases
case unexpected(code: Int)
}
Extend A Custom Error
Like all Swift types, new custom error types you create can be extended to add computed properties and functions. In this example, the isFatal
computed property is added that can be used to determine if the error is recoverable:
extension CustomError {
var isFatal: Bool {
if case CustomError.unexpected = self { return true }
else { return false }
}
}
description for Custom Errors Using CustomStringConvertible
Custom errors implemented in Swift can have custom descriptions for each error. To add a description to a new error type, extend the custom error to conform to CustomStringConvertible
and add a property description
:
// For each error type return the appropriate description
extension CustomError: CustomStringConvertible {
public var description: String {
switch self {
case .invalidPassword:
return "The provided password is not valid."
case .notFound:
return "The specified item could not be found."
case .unexpected(_):
return "An unexpected error occurred."
}
}
}
localizedDescription For Custom Errors Using LocalizedError
New custom errors you create in Swift can also have localized custom descriptions for each error. To add a localized description to a new error type, extend the custom error to conform to LocalizedError
and add a property errorDescription
:
// For each error type return the appropriate localized description
extension CustomError: LocalizedError {
public var errorDescription: String? {
switch self {
case .invalidPassword:
return NSLocalizedString(
"The provided password is not valid.",
comment: "Invalid Password"
)
case .notFound:
return NSLocalizedString(
"The specified item could not be found.",
comment: "Resource Not Found"
)
case .unexpected(_):
return NSLocalizedString(
"An unexpected error occurred.",
comment: "Unexpected Error"
)
}
}
}
Throw Custom Errors
Functions marked as throws
in Swift can throw custom errors directly:
func isAvailable(resourcePath: String) throws {
if FileManager.default.fileExists(atPath: path) {
return true
}
else {
throw CustomError.notFound
}
}
Catch Custom Errors
Custom errors can be individually caught and handled using the do
catch
syntax. Use catch
followed by the specific error to catch the error and apply specific handling logic:
func open(resourcePath: String) {
do {
try isAvailable(resourcePath: resourcePath)
// Handle opening the resource
}
catch CustomError.notFound {
// Handle custom error
}
catch {
// Handle other errors
}
}
Create Your Own Errors In Swift
That’s it! By conforming to Error
, CustomStringConvertible
, and LocalizedError
you can implement descriptive, clear, and actionable custom errors in Swift.
Error handling is the process of responding to and recovering from error conditions in your program. Swift provides first-class support for throwing, catching, propagating, and manipulating recoverable errors at runtime.
Some operations aren’t guaranteed to always complete execution or produce a useful output. Optionals are used to represent the absence of a value, but when an operation fails, it’s often useful to understand what caused the failure, so that your code can respond accordingly.
As an example, consider the task of reading and processing data from a file on disk. There are a number of ways this task can fail, including the file not existing at the specified path, the file not having read permissions, or the file not being encoded in a compatible format. Distinguishing among these different situations allows a program to resolve some errors and to communicate to the user any errors it can’t resolve.
Representing and Throwing Errors¶
In Swift, errors are represented by values of types that conform to the Error
protocol. This empty protocol indicates that a type can be used for error handling.
Swift enumerations are particularly well suited to modeling a group of related error conditions, with associated values allowing for additional information about the nature of an error to be communicated. For example, here’s how you might represent the error conditions of operating a vending machine inside a game:
- enum VendingMachineError: Error {
- case invalidSelection
- case insufficientFunds(coinsNeeded: Int)
- case outOfStock
- }
Throwing an error lets you indicate that something unexpected happened and the normal flow of execution can’t continue. You use a throw
statement to throw an error. For example, the following code throws an error to indicate that five additional coins are needed by the vending machine:
- throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
Handling Errors¶
When an error is thrown, some surrounding piece of code must be responsible for handling the error—for example, by correcting the problem, trying an alternative approach, or informing the user of the failure.
There are four ways to handle errors in Swift. You can propagate the error from a function to the code that calls that function, handle the error using a do
—catch
statement, handle the error as an optional value, or assert that the error will not occur. Each approach is described in a section below.
When a function throws an error, it changes the flow of your program, so it’s important that you can quickly identify places in your code that can throw errors. To identify these places in your code, write the try
keyword—or the try?
or try!
variation—before a piece of code that calls a function, method, or initializer that can throw an error. These keywords are described in the sections below.
Note
Error handling in Swift resembles exception handling in other languages, with the use of the try
, catch
and throw
keywords. Unlike exception handling in many languages—including Objective-C—error handling in Swift doesn’t involve unwinding the call stack, a process that can be computationally expensive. As such, the performance characteristics of a throw
statement are comparable to those of a return
statement.
Propagating Errors Using Throwing Functions¶
To indicate that a function, method, or initializer can throw an error, you write the throws
keyword in the function’s declaration after its parameters. A function marked with throws
is called a throwing function. If the function specifies a return type, you write the throws
keyword before the return arrow (->
).
- func canThrowErrors() throws -> String
- func cannotThrowErrors() -> String
A throwing function propagates errors that are thrown inside of it to the scope from which it’s called.
Note
Only throwing functions can propagate errors. Any errors thrown inside a nonthrowing function must be handled inside the function.
In the example below, the VendingMachine
class has a vend(itemNamed:)
method that throws an appropriate VendingMachineError
if the requested item isn’t available, is out of stock, or has a cost that exceeds the current deposited amount:
- struct Item {
- var price: Int
- var count: Int
- }
- class VendingMachine {
- var inventory = [
- «Candy Bar»: Item(price: 12, count: 7),
- «Chips»: Item(price: 10, count: 4),
- «Pretzels»: Item(price: 7, count: 11)
- ]
- var coinsDeposited = 0
- func vend(itemNamed name: String) throws {
- guard let item = inventory[name] else {
- throw VendingMachineError.invalidSelection
- }
- guard item.count > 0 else {
- throw VendingMachineError.outOfStock
- }
- guard item.price <= coinsDeposited else {
- throw VendingMachineError.insufficientFunds(coinsNeeded: item.price — coinsDeposited)
- }
- coinsDeposited -= item.price
- var newItem = item
- newItem.count -= 1
- inventory[name] = newItem
- print(«Dispensing (name)«)
- }
- }
The implementation of the vend(itemNamed:)
method uses guard
statements to exit the method early and throw appropriate errors if any of the requirements for purchasing a snack aren’t met. Because a throw
statement immediately transfers program control, an item will be vended only if all of these requirements are met.
Because the vend(itemNamed:)
method propagates any errors it throws, any code that calls this method must either handle the errors—using a do
—catch
statement, try?
, or try!
—or continue to propagate them. For example, the buyFavoriteSnack(person:vendingMachine:)
in the example below is also a throwing function, and any errors that the vend(itemNamed:)
method throws will propagate up to the point where the buyFavoriteSnack(person:vendingMachine:)
function is called.
- let favoriteSnacks = [
- «Alice»: «Chips»,
- «Bob»: «Licorice»,
- «Eve»: «Pretzels»,
- ]
- func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
- let snackName = favoriteSnacks[person] ?? «Candy Bar»
- try vendingMachine.vend(itemNamed: snackName)
- }
In this example, the buyFavoriteSnack(person: vendingMachine:)
function looks up a given person’s favorite snack and tries to buy it for them by calling the vend(itemNamed:)
method. Because the vend(itemNamed:)
method can throw an error, it’s called with the try
keyword in front of it.
Throwing initializers can propagate errors in the same way as throwing functions. For example, the initializer for the PurchasedSnack
structure in the listing below calls a throwing function as part of the initialization process, and it handles any errors that it encounters by propagating them to its caller.
- struct PurchasedSnack {
- let name: String
- init(name: String, vendingMachine: VendingMachine) throws {
- try vendingMachine.vend(itemNamed: name)
- self.name = name
- }
- }
Handling Errors Using Do-Catch¶
You use a do
—catch
statement to handle errors by running a block of code. If an error is thrown by the code in the do
clause, it’s matched against the catch
clauses to determine which one of them can handle the error.
Here is the general form of a do
—catch
statement:
- do {
- try expression
- statements
- } catch pattern 1 {
- statements
- } catch pattern 2 where condition {
- statements
- } catch pattern 3, pattern 4 where condition {
- statements
- } catch {
- statements
- }
You write a pattern after catch
to indicate what errors that clause can handle. If a catch
clause doesn’t have a pattern, the clause matches any error and binds the error to a local constant named error
. For more information about pattern matching, see Patterns.
For example, the following code matches against all three cases of the VendingMachineError
enumeration.
- var vendingMachine = VendingMachine()
- vendingMachine.coinsDeposited = 8
- do {
- try buyFavoriteSnack(person: «Alice», vendingMachine: vendingMachine)
- print(«Success! Yum.»)
- } catch VendingMachineError.invalidSelection {
- print(«Invalid Selection.»)
- } catch VendingMachineError.outOfStock {
- print(«Out of Stock.»)
- } catch VendingMachineError.insufficientFunds(let coinsNeeded) {
- print(«Insufficient funds. Please insert an additional (coinsNeeded) coins.»)
- } catch {
- print(«Unexpected error: (error).»)
- }
- // Prints «Insufficient funds. Please insert an additional 2 coins.»
In the above example, the buyFavoriteSnack(person:vendingMachine:)
function is called in a try
expression, because it can throw an error. If an error is thrown, execution immediately transfers to the catch
clauses, which decide whether to allow propagation to continue. If no pattern is matched, the error gets caught by the final catch
clause and is bound to a local error
constant. If no error is thrown, the remaining statements in the do
statement are executed.
The catch
clauses don’t have to handle every possible error that the code in the do
clause can throw. If none of the catch
clauses handle the error, the error propagates to the surrounding scope. However, the propagated error must be handled by some surrounding scope. In a nonthrowing function, an enclosing do
—catch
statement must handle the error. In a throwing function, either an enclosing do
—catch
statement or the caller must handle the error. If the error propagates to the top-level scope without being handled, you’ll get a runtime error.
For example, the above example can be written so any error that isn’t a VendingMachineError
is instead caught by the calling function:
- func nourish(with item: String) throws {
- do {
- try vendingMachine.vend(itemNamed: item)
- } catch is VendingMachineError {
- print(«Couldn’t buy that from the vending machine.»)
- }
- }
- do {
- try nourish(with: «Beet-Flavored Chips»)
- } catch {
- print(«Unexpected non-vending-machine-related error: (error)«)
- }
- // Prints «Couldn’t buy that from the vending machine.»
In the nourish(with:)
function, if vend(itemNamed:)
throws an error that’s one of the cases of the VendingMachineError
enumeration, nourish(with:)
handles the error by printing a message. Otherwise, nourish(with:)
propagates the error to its call site. The error is then caught by the general catch
clause.
Another way to catch several related errors is to list them after catch
, separated by commas. For example:
- func eat(item: String) throws {
- do {
- try vendingMachine.vend(itemNamed: item)
- } catch VendingMachineError.invalidSelection, VendingMachineError.insufficientFunds, VendingMachineError.outOfStock {
- print(«Invalid selection, out of stock, or not enough money.»)
- }
- }
The eat(item:)
function lists the vending machine errors to catch, and its error text corresponds to the items in that list. If any of the three listed errors are thrown, this catch
clause handles them by printing a message. Any other errors are propagated to the surrounding scope, including any vending-machine errors that might be added later.
Converting Errors to Optional Values¶
You use try?
to handle an error by converting it to an optional value. If an error is thrown while evaluating the try?
expression, the value of the expression is nil
. For example, in the following code x
and y
have the same value and behavior:
- func someThrowingFunction() throws -> Int {
- // …
- }
- let x = try? someThrowingFunction()
- let y: Int?
- do {
- y = try someThrowingFunction()
- } catch {
- y = nil
- }
If someThrowingFunction()
throws an error, the value of x
and y
is nil
. Otherwise, the value of x
and y
is the value that the function returned. Note that x
and y
are an optional of whatever type someThrowingFunction()
returns. Here the function returns an integer, so x
and y
are optional integers.
Using try?
lets you write concise error handling code when you want to handle all errors in the same way. For example, the following code uses several approaches to fetch data, or returns nil
if all of the approaches fail.
- func fetchData() -> Data? {
- if let data = try? fetchDataFromDisk() { return data }
- if let data = try? fetchDataFromServer() { return data }
- return nil
- }
Disabling Error Propagation¶
Sometimes you know a throwing function or method won’t, in fact, throw an error at runtime. On those occasions, you can write try!
before the expression to disable error propagation and wrap the call in a runtime assertion that no error will be thrown. If an error actually is thrown, you’ll get a runtime error.
For example, the following code uses a loadImage(atPath:)
function, which loads the image resource at a given path or throws an error if the image can’t be loaded. In this case, because the image is shipped with the application, no error will be thrown at runtime, so it’s appropriate to disable error propagation.
- let photo = try! loadImage(atPath: «./Resources/John Appleseed.jpg»)
Specifying Cleanup Actions¶
You use a defer
statement to execute a set of statements just before code execution leaves the current block of code. This statement lets you do any necessary cleanup that should be performed regardless of how execution leaves the current block of code—whether it leaves because an error was thrown or because of a statement such as return
or break
. For example, you can use a defer
statement to ensure that file descriptors are closed and manually allocated memory is freed.
A defer
statement defers execution until the current scope is exited. This statement consists of the defer
keyword and the statements to be executed later. The deferred statements may not contain any code that would transfer control out of the statements, such as a break
or a return
statement, or by throwing an error. Deferred actions are executed in the reverse of the order that they’re written in your source code. That is, the code in the first defer
statement executes last, the code in the second defer
statement executes second to last, and so on. The last defer
statement in source code order executes first.
- func processFile(filename: String) throws {
- if exists(filename) {
- let file = open(filename)
- defer {
- close(file)
- }
- while let line = try file.readline() {
- // Work with the file.
- }
- // close(file) is called here, at the end of the scope.
- }
- }
The above example uses a defer
statement to ensure that the open(_:)
function has a corresponding call to close(_:)
.
Note
You can use a defer
statement even when no error handling code is involved.
Сколько раз мы всматривались в этот код:
do {
try writeEverythingToDisk()
} catch let error {
// ???
}
или в этот:
switch result {
case .failure(let error):
// ???
}
и задавали себе вопрос: “Как же мне выудить из этой ошибки информацию?”
Проблема в том, что ошибка, вероятно, содержит много информации, которая могла бы нам помочь. Но получение этой информации часто дело не простое.
Чтобы понять причину этого, давайте рассмотрим имеющиеся в нашем распоряжении способы прикрепления информации к ошибкам.
Новое: LocalizedError
В языке Swift мы передаем ошибки в соответствии с протоколом Error. Протокол LocalizedError его наследует, расширяя его некоторыми полезными свойствами:
- errorDescription
- failureReason
- recoverySuggestion
Соответствие протоколу LocalizedError вместо протокола Error (и обеспечение реализации этих новых свойств), позволяет нам дополнить нашу ошибку множеством полезной информации, которая может быть передана во время исполнения программы (интернет-журнал NSHipster рассматривает этот вопрос более подробно):
enum MyError: LocalizedError {
case badReference
var errorDescription: String? {
switch self {
case .badReference:
return "The reference was bad."
}
}
var failureReason: String? {
switch self {
case .badReference:
return "Bad Reference"
}
}
var recoverySuggestion: String? {
switch self {
case .badReference:
return "Try using a good one."
}
}
}
Старое: userInfo
Хорошо знакомый класс NSError содержит свойство — словарь userInfo, который мы можем заполнить всем, чем захотим. Но также, этот словарь содержит несколько заранее определённых ключей:
- NSLocalizedDescriptionKey
- NSLocalizedFailureReasonErrorKey
- NSLocalizedRecoverySuggestionErrorKey
Можно заметить, что их названия очень похожи на свойства LocalizedError. И, фактически, они играют аналогичную роль:
let info = [
NSLocalizedDescriptionKey:
"The reference was bad.",
NSLocalizedFailureReasonErrorKey:
"Bad Reference",
NSLocalizedRecoverySuggestionErrorKey:
"Try using a good one."
]
let badReferenceNSError = NSError(
domain: "ReferenceDomain",
code: 42,
userInfo: info
)
Это выглядит, как будто LocalizedError и NSError должны быть в основном равнозначны, верно? Что ж, в этом-то и заключается основная проблема.
Старое встречается с новым
Дело в том, что класс NSError соответствует протоколу Error, но не протоколу LocalizedError. Иными словами:
badReferenceNSError is NSError //> true
badReferenceNSError is Error //> true
badReferenceNSError is LocalizedError //> false
Это значит, что если мы попытаемся извлечь информацию из любой произвольной ошибки привычным способом, то это сработает должным образом только для Error и LocalizedError, но для NSError будет отражено только значение свойства localizedDescription:
// The obvious way that doesn’t work:
func log(error: Error) {
print(error.localizedDescription)
if let localized = error as? LocalizedError {
print(localized.failureReason)
print(localized.recoverySuggestion)
}
}
log(error: MyError.badReference)
//> The reference was bad.
//> Bad Reference
//> Try using a good one.
log(error: badReferenceNSError)
//> The reference was bad.
Это довольно неприятно, потому как известно, что наш объект класса NSError содержит в себе информацию о причине сбоя и предложение по исправлению ошибки, прописанные в его словаре userInfo. И это, по какой-то причине, не отображается через соответствие LocalizedError.
Новое становится старым
В этом месте мы можем впасть в отчаяние, мысленно представляя себе множество операторов switch, пытающихся отсортировать по типам и по наличию различные свойства словаря userInfo. Но не бойтесь! Есть несложное решение. Просто оно не совсем очевидно.
Обратите внимание, что в классе NSError определены удобные методы для извлечения локализованного описания, причины сбоя и предложения по восстановлению в свойстве userInfo:
badReferenceNSError.localizedDescription
//> "The reference was bad."
badReferenceNSError.localizedFailureReason
//> "Bad Reference"
badReferenceNSError.localizedRecoverySuggestion
//> "Try using a good one."
Они отлично подходят для обработки NSError, но не помогают нам извлечь эти значения из LocalizedError… или это так?
Оказывается, протокол языка Swift Error соединён компилятором с классом NSError. Это означает, что мы можем превратить Error в NSError с помощью простого приведения типа:
let bridgedError: NSError
bridgedError = MyError.badReference as NSError
Но ещё больше впечатляет то, что когда мы производим приведение LocalizedError этим способом, то мост срабатывает правильно и подключает localizedDescription, localizedFailureReason и localizedRecoverySuggestion, указывая на соответствующие значения!
Поэтому, если мы хотим, чтобы согласованный интерфейс извлекал локализованную информацию из Error, LocalizedError и NSError, нам просто нужно не долго думая привести всё к NSError:
func log(error: Error) {
let bridge = error as NSError
print(bridge.localizedDescription)
print(bridge.localizedFailureReason)
print(bridge.localizedRecoverySuggestion)
}
log(error: MyError.badReference)
//> The reference was bad.
//> Bad Reference
//> Try using a good one.
log(error: badReferenceNSError)
//> The reference was bad.
//> Bad Reference
//> Try using a good one.
Готово!
Оригинал статьи
Содержание
- Значения локализованных ошибок в практике Swift
- Значения локализованных ошибок в практике Swift
- Новое: LocalizedError
- Старое: userInfo
- Старое встречается с новым
- Новое становится старым
- Create, Throw, and Handle Custom Errors In Swift
- Create A Custom Error And Conform To The Error Type
- Extend A Custom Error
- description for Custom Errors Using CustomStringConvertible
- localizedDescription For Custom Errors Using LocalizedError
- Throw Custom Errors
- Catch Custom Errors
- Create Your Own Errors In Swift
- Play A Sound Using AVAudioPlayer In Swift
- Rounding Floats and Doubles in Swift
- Follow Advanced Swift For FREE
- Error Handling in Swift
- Error Conditions
- Logical Error Conditions
- Simple Error Conditions
- Recoverable Error Conditions
- The ErrorProtocol Protocol
- Using Result Types
- Throwing Errors with throw
- Error Handling the 10,000ft View
- Propagating Errors
- Error Propagation with throws
- Calling Throwing Functions
- Throwing Functions and Inheritance
- Propagating Errors with rethrows
- Handling Errors
- Handling Errors with do-catch Statement
- Converting Errors Into Optional Values with try?
- Preventing Error Propagation with try!
- Error Propagation With Asynchronous Closures
- Wrapping Up
Значения локализованных ошибок в практике Swift
Значения локализованных ошибок в практике Swift
Сколько раз мы всматривались в этот код:
и задавали себе вопрос: “Как же мне выудить из этой ошибки информацию?”
Проблема в том, что ошибка, вероятно, содержит много информации, которая могла бы нам помочь. Но получение этой информации часто дело не простое.
Чтобы понять причину этого, давайте рассмотрим имеющиеся в нашем распоряжении способы прикрепления информации к ошибкам.
Новое: LocalizedError
В языке Swift мы передаем ошибки в соответствии с протоколом Error . Протокол LocalizedError его наследует, расширяя его некоторыми полезными свойствами:
- errorDescription
- failureReason
- recoverySuggestion
Соответствие протоколу LocalizedError вместо протокола Error (и обеспечение реализации этих новых свойств), позволяет нам дополнить нашу ошибку множеством полезной информации, которая может быть передана во время исполнения программы (интернет-журнал NSHipster рассматривает этот вопрос более подробно):
Старое: userInfo
Хорошо знакомый класс NSError содержит свойство — словарь userInfo , который мы можем заполнить всем, чем захотим. Но также, этот словарь содержит несколько заранее определённых ключей:
- NSLocalizedDescriptionKey
- NSLocalizedFailureReasonErrorKey
- NSLocalizedRecoverySuggestionErrorKey
Можно заметить, что их названия очень похожи на свойства LocalizedError . И, фактически, они играют аналогичную роль:
Это выглядит, как будто LocalizedError и NSError должны быть в основном равнозначны, верно? Что ж, в этом-то и заключается основная проблема.
Старое встречается с новым
Дело в том, что класс NSError соответствует протоколу Error , но не протоколу LocalizedError . Иными словами:
Это значит, что если мы попытаемся извлечь информацию из любой произвольной ошибки привычным способом, то это сработает должным образом только для Error и LocalizedError , но для NSError будет отражено только значение свойства localizedDescription :
Это довольно неприятно, потому как известно, что наш объект класса NSError содержит в себе информацию о причине сбоя и предложение по исправлению ошибки, прописанные в его словаре userInfo . И это, по какой-то причине, не отображается через соответствие LocalizedError .
Новое становится старым
В этом месте мы можем впасть в отчаяние, мысленно представляя себе множество операторов switch , пытающихся отсортировать по типам и по наличию различные свойства словаря userInfo . Но не бойтесь! Есть несложное решение. Просто оно не совсем очевидно.
Обратите внимание, что в классе NSError определены удобные методы для извлечения локализованного описания, причины сбоя и предложения по восстановлению в свойстве userInfo :
Они отлично подходят для обработки NSError , но не помогают нам извлечь эти значения из LocalizedError . или это так?
Оказывается, протокол языка Swift Error соединён компилятором с классом NSError . Это означает, что мы можем превратить Error в NSError с помощью простого приведения типа:
Но ещё больше впечатляет то, что когда мы производим приведение LocalizedError этим способом, то мост срабатывает правильно и подключает localizedDescription , localizedFailureReason и localizedRecoverySuggestion , указывая на соответствующие значения!
Поэтому, если мы хотим, чтобы согласованный интерфейс извлекал локализованную информацию из Error , LocalizedError и NSError , нам просто нужно не долго думая привести всё к NSError :
Источник
Create, Throw, and Handle Custom Errors In Swift
Learn how to create, extend, throw, and handle custom error types in Swift.
Creating custom error types is an important skill when designing clear APIs for iOS and macOS applications. This post presents examples for creating, extending, throwing, and catching custom error types in Swift:
Create A Custom Error And Conform To The Error Type
To create a custom error, create an enum in Swift that conforms to the Error protocol. Each case of the enum represents a unique error that can be thrown and handled:
Extend A Custom Error
Like all Swift types, new custom error types you create can be extended to add computed properties and functions. In this example, the isFatal computed property is added that can be used to determine if the error is recoverable:
description for Custom Errors Using CustomStringConvertible
Custom errors implemented in Swift can have custom descriptions for each error. To add a description to a new error type, extend the custom error to conform to CustomStringConvertible and add a property description :
localizedDescription For Custom Errors Using LocalizedError
New custom errors you create in Swift can also have localized custom descriptions for each error. To add a localized description to a new error type, extend the custom error to conform to LocalizedError and add a property errorDescription :
Throw Custom Errors
Functions marked as throws in Swift can throw custom errors directly:
Catch Custom Errors
Custom errors can be individually caught and handled using the do catch syntax. Use catch followed by the specific error to catch the error and apply specific handling logic:
Create Your Own Errors In Swift
That’s it! By conforming to Error , CustomStringConvertible , and LocalizedError you can implement descriptive, clear, and actionable custom errors in Swift.
Play A Sound Using AVAudioPlayer In Swift
Learn how to load a sound file or sound effect, configure an AVSession to play sound, and play a sound file using AVAudioPlayer in Swift.
Rounding Floats and Doubles in Swift
Learn how to round decimal numbers up, down, and with custom decimal precision in Swift.
Follow Advanced Swift For FREE
Follow Advanced Swift for post updates and FREE access to commented code for custom Swift animations.
Источник
Error Handling in Swift
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.
Table of Contents
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:
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:
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:
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:
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:
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:
We can instead, re-define our function to return a ResultType value that is parameterised over the String type:
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:
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:
If we then parameterise our generic type over our more constrained FileError type as well as the String type:
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:
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:
In Swift, throw statements can occur in the body of any function or method or within the body of any closure expression. (To make things easier I’m just going to refer to these all as functions for now). When executed, throw statements cause execution of the current program scope to immediately end and the thrown error value to be propagated to the enclosing scope.
Error Handling the 10,000ft View
Ok. So far so good. We know how to throw an error but what do we do when these error values are actually thrown?
Generally speaking errors are not what we want to encounter in our code and when errors do occur (and trust me, they will) it is up to us to write code to detect and handle those errors.
The act of handling an error in Swift is known as catching it. So we’ve got throwing to create and propagate an error and catching to detect and handle the error. Throw. Catch. Get it?
Anyway, exactly where and when we handle errors in our code is up to us but there is one golden rule – not handling errors – ignoring them and hoping they go away – is not an option. The Swift compiler is actually pretty strict on this topic and requires that we handle all errors at some point in our chain of function calls – so much so that it won’t compile our code if we don’t!
Ok, so we have to handle and catch errors when they’re thrown but how exactly do we do that?
In Swift, we have two main courses of action when it comes to catching and handling errors:
- Propagate the error to another function further up the call chain (a.k.a. make some other piece of code handle it).
- Handle the error within the scope of the current function.
Which approach we take is dependent on what makes most sense for the particular situation but in the next few sections, we’ll take a closer look at each so we can make a more informed decision about which to choose. Let’s start by looking at how to propagate errors.
Propagating Errors
Error Propagation with throws
In Swift, whether or not a function can propagate an error is baked into the functions signature.
By default, a function in Swift cannot propagate errors. This applies to all types of function whether they be global functions, methods or closures. It also means that we must, by default, write code to catch any errors that may occur and handle them within the body of a function in which they occur.
Note: In the following sections I again refer to functions but this equally applies to methods and closures as well.
Despite these rules, sometimes it’s just not possible to handle errors within the scope of the current function. Maybe we don’t have enough information to handle it at that level or maybe it just doesn’t makes sense to do so. Whatever the reason, there are occasions when we want to propagate errors that have occurred within a function back to the code that called that function.
To indicate that a function may propagate an error back to it’s calling scope, Swift requires that we annotate such functions with the throws (or as we’ll see shortly – rethrows ) keyword. Functions that are annotated in this manner are unsurprisingly known as throwing functions.
To annotate a function with the throws (or rethrows ) keyword we write the keyword after the parameters in the functions declaration and, if the function returns a value, before the return arrow ( -> ). For example, we could re-write our fileContents(filename:) example as follows:
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:
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:
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() :
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:
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:
Now imagine we had a child class ( Qux ) that inherited from Baz and overrode the doSomethingElse() method:
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:
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:
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:
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:
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:
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:
We can use this in various forms to to catch different types of error should they occur:
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.:
We also use an enumeration case pattern to match the .mileageTooHigh error value and extract any associated mileage:
We can also combine that with a where clause to further constrain things as we’ve done in the second catch clause:
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:
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:
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:
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:
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:
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:
Another option, and one that is possible but feels like a bit of a hack is to us an inner closure:
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.
Источник