ErrorType
is a big thing is Swift, changing the way how errors are handled.
NSError is the Past
NSError
was the way how errors are encapsulated, and passed along in methods and blocks/closures.
But it was not a good design.
It differentiates the errors using code
(Int), and also domain
(String).
And it passes more information in userInfo
as a dictionary.
It works, but isn’t cool.
A bit of History: In the very beginning (in MacOS, way before any iOS), there is NSException and try-catch in Objective-C, but eventually NSError took over.
ErrorType
Then comes ErrorType
in Swift.
It is merely a protocol
.
And it does NOT have any method. It is an EMPTY PROTOCOL.
That means you can make any of your type a ErrorType
, and have it being throw around.
Note: ErrorType
can always be casted to NSError
, magically. You can cast to NSError
without any problem, but it don’t make much sense because you lose almost all information on the error.
If you choose to use ErrorType
, forget about NSError
. Unless the NSError
comes from other frameworks/libraries, then you might want to handle and map to one of your custom ErrorType
case. Or you can wrap the NSError in your custom ErrorType
(see .OtherNSError
below).
Custom ErrorType
Every app should have one (or more) custom AppError
that subclass ErrorType
.
It will usually be an enum (but you can use class and struct too, if you want), with different cases of errors, and init with different parameters (thanks to enum)!
enum AppError: ErrorType {
case DivisionError
case NetworkError(code: Int)
case UnexpectedError(message: String)
case OtherNSError(nsError: NSError)
}
It will be good to implement CustomStringConvertible
, and return description
accordingly for each error case.
try-catch-throw
I will not discuss more on how try-catch-throw works, because there are many, and I love this version:
An Example
But I will provide an example using Unbox library (a good JSON decoder) of how to catch it’s UnboxError
(their custom ErrorType
):
do {
...
} catch UnboxError.MissingKey(let key) {
// Specific error case of a MissingKey
print("Missing Key", key)
} catch let error as UnboxError {
// Catch all other UnboxError
// Because UnboxError conforms to CustomStringConvertible, we can print `description`
print(error.description)
} catch let error as NSError {
// Warning: Cast to NSError (details will be lost). Not recommended.
print(error.localizedDescription)
}
Description of the Error
Given an error, we usually display error.localizedDescription
to the user.
There are default implementations, but you will want to implement your own when you have meaningful messages that can be displayed. To do so, extend LocalizedError
protocol.
extension AppError: LocalizedError {
var errorDescription: String? {
switch self {
case .unsupportedCountry:
return "This is what went wrong."
default:
return self.localizedDescription
}
}
}
There are other properties you may also extend to provide reasons and recovery.
Handling throw in closures
There are 2 ways as discussed in appventure.me:
- Using
Result
- Using inner closure that throws
Using Result
is simpler, and easier to read. It became popular as more developers use the Result
, even though it is NOT in Swift standard library. It is a simple concept, and you can find a couple of different Result.swift
in Github.
Result Type
Result
is an enumeration that’s either a .Success(value)
or a .Failure(error)
, and is a common pattern among Swift programmers.
Before Swift 2, it is a common way to deal with a result - either success or with error.
In Swift 2, try-catch is Apple’s endorsed way.
But Result
still is good and more composable eg. you can pass Result
in a closure
The concept of Result
is very simple, and the shortest implementation is:
public enum Result<T, Error: ErrorType> {
case Success(T)
case Failure(Error)
}
But you may want to refer to a more complete implementation, which has more feature such as dematerialize
to throw an error if it is a failure.
Many frameworks, such as Alamofire, will also write their own Result
type. You cam probably write your own too.