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.
I was living under a rock until very recently and believed that the Error protocol was truly empty with no requirements. While it is true that there are no requirements, it does define a localizedDescription String property, as a quick pit stop at its documentation page shows.
This property, strangely, does not find mention in the Error handling section of Apple’s Swift book.
So, how does an error defined like
enum GraveError: Error {
case learningSwift
}
supply a localizedDescription?
To understand this, I did some digging and found two great resources – the NSError bridging evolution proposal and the NSError.swift file in the open source Foundation library that is used on non-Apple platforms.
The relevant protocol extension is:
public extension Error {
/// Retrieve the localized description for this error.
var localizedDescription: String {
if let nsError = self as? NSError {
return nsError.localizedDescription
}
let defaultUserInfo = _swift_Foundation_getErrorDefaultUserInfo(self) as? [String : Any]
return NSError(domain: _domain, code: _code, userInfo: defaultUserInfo).localizedDescription
}
}
Before continuing, allow me to take a small detour about the late great NSError. Behind the scenes, NSError also conforms to Error (it is empty after all), and always had a localizedDescription. This description is derived from what you put in an NSError’s userInfo dictionary, specifically the value for the NSLocalizedErrorDescriptionKey.
So, if the error is an NSError object, it just returns that object’s localizedDescription.
If it is not an NSError, and just a good old Swift Error, it creates a user info dictionary, then creates an NSError out of it, and then returns its localizedDescription.
This is where the LocalizedError protocol enters the stage.
The LocalizedError protocol inherits from the empty Error protocol and adds four requirements of its own (empty defaults are provided in extensions), one of which is the well named errorDescription. In the process of creating the afore-mentioned user info dictionary, this errorDescription is used for the NSLocalizedErrorDescriptionKey, if the error happens to be a LocalizedError.
There are more details in NSError.swift, which I leave for the interested reader.
The long and short is this: if you are in possession of any error (whether an NSError or Error), you can print its localizedDescription. For your NSErrors, populate the user info dictionary correctly and correspondingly, it does not hurt to get into the habit of conforming all your Swift Errors to LocalizedErrors and providing an errorDescription, like so:
extension GraveError: LocalizedError {
var errorDescription: String? {
switch self {
case .learningSwift: return "Haha, good joke."
}
}
}
If not, you can end up with the (not so) dreaded “The operation couldn’t be completed __lldb_expr xxxx code x” message.
Good luck making mistakes!
Сколько раз мы всматривались в этот код:
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.
Готово!
Оригинал статьи
As described in the Xcode 8 beta 6 release notes,
Swift-defined error types can provide localized error descriptions by adopting the new LocalizedError protocol.
In your case:
public enum MyError: Error {
case customError
}
extension MyError: LocalizedError {
public var errorDescription: String? {
switch self {
case .customError:
return NSLocalizedString("A user-friendly description of the error.", comment: "My error")
}
}
}
let error: Error = MyError.customError
print(error.localizedDescription) // A user-friendly description of the error.
You can provide even more information if the error is converted
to NSError
(which is always possible):
extension MyError : LocalizedError {
public var errorDescription: String? {
switch self {
case .customError:
return NSLocalizedString("I failed.", comment: "")
}
}
public var failureReason: String? {
switch self {
case .customError:
return NSLocalizedString("I don't know why.", comment: "")
}
}
public var recoverySuggestion: String? {
switch self {
case .customError:
return NSLocalizedString("Switch it off and on again.", comment: "")
}
}
}
let error = MyError.customError as NSError
print(error.localizedDescription) // I failed.
print(error.localizedFailureReason) // Optional("I don't know why.")
print(error.localizedRecoverySuggestion) // Optional("Switch it off and on again.")
By adopting the CustomNSError
protocol the error can provide
a userInfo
dictionary (and also a domain
and code
). Example:
extension MyError: CustomNSError {
public static var errorDomain: String {
return "myDomain"
}
public var errorCode: Int {
switch self {
case .customError:
return 999
}
}
public var errorUserInfo: [String : Any] {
switch self {
case .customError:
return [ "line": 13]
}
}
}
let error = MyError.customError as NSError
if let line = error.userInfo["line"] as? Int {
print("Error in line", line) // Error in line 13
}
print(error.code) // 999
print(error.domain) // myDomain
Using a struct can be an alternative. A little bit elegance with static localization:
import Foundation
struct MyError: LocalizedError, Equatable {
private var description: String!
init(description: String) {
self.description = description
}
var errorDescription: String? {
return description
}
public static func ==(lhs: MyError, rhs: MyError) -> Bool {
return lhs.description == rhs.description
}
}
extension MyError {
static let noConnection = MyError(description: NSLocalizedString("No internet connection",comment: ""))
static let requestFailed = MyError(description: NSLocalizedString("Request failed",comment: ""))
}
func throwNoConnectionError() throws {
throw MyError.noConnection
}
do {
try throwNoConnectionError()
}
catch let myError as MyError {
switch myError {
case .noConnection:
print("noConnection: (myError.localizedDescription)")
case .requestFailed:
print("requestFailed: (myError.localizedDescription)")
default:
print("default: (myError.localizedDescription)")
}
}
I would also add, if your error has parameters like this
enum NetworkError: LocalizedError {
case responseStatusError(status: Int, message: String)
}
you can call these parameters in your localized description like this:
extension NetworkError {
public var errorDescription: String? {
switch self {
case .responseStatusError(status: let status, message: let message):
return "Error with status (status) and message (message) was thrown"
}
}
You can even make this shorter like this:
extension NetworkError {
public var errorDescription: String? {
switch self {
case let .responseStatusError(status, message):
return "Error with status (status) and message (message) was thrown"
}
}
There are now two Error-adopting protocols that your error type can adopt in order to provide additional information to Objective-C — LocalizedError and CustomNSError. Here’s an example error that adopts both of them:
enum MyBetterError : CustomNSError, LocalizedError {
case oops
// domain
static var errorDomain : String { return "MyDomain" }
// code
var errorCode : Int { return -666 }
// userInfo
var errorUserInfo: [String : Any] { return ["Hey":"Ho"] };
// localizedDescription
var errorDescription: String? { return "This sucks" }
// localizedFailureReason
var failureReason: String? { return "Because it sucks" }
// localizedRecoverySuggestion
var recoverySuggestion: String? { return "Give up" }
}
# Error handling basics
Functions in Swift may return values, throw errors (opens new window), or both:
Any value which conforms to the ErrorType protocol (opens new window) (including NSError objects) can be thrown as an error. Enumerations (opens new window) provide a convenient way to define custom errors:
An error indicates a non-fatal failure during program execution, and is handled with the specialized control-flow constructs do
/catch
, throw
, and try
.
Errors can be caught with do
/catch
:
Any function which can throw an error must be called using try
, try?
, or try!
:
# Catching different error types
Let’s create our own error type for this example.
The Do-Catch syntax allows to catch a thrown error, and automatically creates a constant named error
available in the catch
block:
You can also declare a variable yourself:
It’s also possible to chain different catch
statements. This is convenient if several types of errors can be thrown in the Do block.
Here the Do-Catch will first attempt to cast the error as a CustomError
, then as an NSError
if the custom type was not matched.
In Swift 3, no need to explicitly downcast to NSError.
# Catch and Switch Pattern for Explicit Error Handling
In the client class:
# Disabling Error Propagation
The creators of Swift have put a lot of attention into making the language expressive and error handling is exactly that, expressive. If you try to invoke a function that can throw an error, the function call needs to be preceded by the try keyword. The try keyword isn’t magical. All it does, is make the developer aware of the throwing ability of the function.
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 is appropriate to disable error propagation.
# Create custom Error with localized description
Create enum
of custom errors
Create extension
of RegistrationError
to handle the Localized description.
Handle error:
For more information about errors, see The Swift Programming Language (opens new window).