Error dealing with fundamentals in Swift
The best way of dealing with errors modified lots for the reason that first model of Swift. The primary massive milestone occurred in Swift 2, the place Apple utterly revamped error administration. These days you should use the do
, attempt
, catch
, throw
, throws
, rethrows
key phrases as an alternative of coping with nasty NSError pointers, so this was a warmly welcomed addition for the language. Now in Swift 5 we take one other big leap ahead by introducing the End result sort as a built-in generic. First, let me present you all the perfect practices of error dealing with within the Swift programming language, subsequent I will present you some cool stuff through the use of outcomes to take care of errors. 🚧
Optionals as error indicators
For easy eventualities you possibly can at all times use non-obligatory values, to point that one thing unhealthy occurred. Additionally the guard
assertion is extraordinarily useful for conditions like this.
let zeroValue = Int("0")!
let nilValue = Int("not a quantity")
guard let quantity = Int("6") else {
fatalError("Ooops... this could at all times work, so we crash.")
}
print(quantity)
If you happen to do not actually care in regards to the underlying sort of the error, this strategy is okay, however typically issues can get extra sophisticated, so that you would possibly want some particulars about the issue. Anyway, you possibly can at all times cease the execution by calling the fatalError
methodology, however in case you achieve this, properly… your app will crash. 💥
There are additionally a pair different methods of cease execution course of, however this could possibly be a subject of a standalone publish, so right here is only a fast cheat sheet of obtainable strategies:
precondition(false, "ouch")
preconditionFailure("ouch")
assert(false, "ouch")
assertionFailure("ouch")
fatalError("ouch")
exit(-1)
The important thing distinction between precondition and assertion is that assert will work solely in debug builds, however precondition is evaluated at all times (even in launch builds). Each strategies will set off a deadly error if the situation fails aka. is fake. ⚠️
Throwing errors through the use of the Error protocol
You may outline your personal error sorts by merely confirming to the built-in Error
protocol. Normally most builders use an enum
with a view to outline totally different causes. You may as well have a customized error message in case you conform to the LocalizedError
protocol. Now you are able to throw customized errors, simply use the throw key phrase if you would like to boost an error of your sort, however in case you achieve this in a operate, it’s important to mark that operate as a throwing operate with the throws key phrases. 🤮
enum DivisionError: Error {
case zeroDivisor
}
extension DivisionError: LocalizedError {
public var errorDescription: String? {
swap self {
case .zeroDivisor:
return "Division by zero is sort of problematic. " +
"(https://en.wikipedia.org/wiki/Division_by_zero)"
}
}
}
func divide(_ x: Int, by y: Int) throws -> Int {
guard y != 0 else {
throw DivisionError.zeroDivisor
}
return x / y
}
Nice, so the divide operate above can generate a customized error message. If the divisor is zero it will throw the zeroDivision error case. Now think about the next situation: you are attempting to learn the contents of a file from the disk. There could possibly be a number of kinds of errors associated to permission or file existence, and many others.
Rethrowing Features and Strategies A operate or methodology might be declared with the rethrows key phrase to point that it throws an error provided that one in all it’s operate parameters throws an error. These features and strategies are often known as rethrowing features and rethrowing strategies. Rethrowing features and strategies should have at the least one throwing operate parameter.
Okay, so a throwing operate can emit totally different error sorts, additionally it could possibly propagate all of the parameter errors, however how will we deal with (or ought to I say: catch) these errors?
The do-try-catch syntax
You simply merely must attempt to execute do a throwing operate. So do not belief the grasp, there may be undoubtedly room for making an attempt out issues! Dangerous joke, proper? 😅
do {
let quantity = attempt divide(10, by: 0)
print(quantity)
}
catch let error as DivisionError {
print("Division error handler block")
print(error.localizedDescription)
}
catch {
print("Generic error handler block")
print(error.localizedDescription)
}
As you possibly can see the syntax is fairly easy, you may have a do block, the place you possibly can attempt to execute your throwing features, if one thing goes unsuitable, you possibly can deal with the errors in several catch blocks. By default an error property is on the market inside each catch block, so you do not have to outline one your self by hand. You may nevertheless have catch blocks for particular error sorts by casting them utilizing the let error as MyType
sytnax proper subsequent to the catch key phrase. So at all times attempt first, do not simply do! 🤪
Variations between attempt, attempt? and take a look at!
As we have seen earlier than you possibly can merely attempt to name a operate that throws an error inside a do-catch block. If the operate triggers some type of error, you possibly can put your error dealing with logic contained in the catch block. That is quite simple & easy.
Generally in case you do not actually care in regards to the underlying error, you possibly can merely convert your throwing operate consequence into an non-obligatory through the use of attempt?. With this strategy you will get a 0 consequence if one thing unhealthy occurs, in any other case you will get again your common worth as it’s anticipated. Right here is the instance from above through the use of attempt?:
guard let quantity = attempt? divide(10, by: 2) else {
fatalError("This could work!")
}
print(quantity)
One other method is to stop error propagation through the use of attempt!, however it’s important to be extraordinarily cautious with this strategy, as a result of if the execution of the “tried operate” fails, your utility will merely crash. So use provided that you are completely positive that the operate will not throw an error. ⚠️
let quantity = attempt! divide(10, by: 2)
print(quantity)
There are a number of locations the place it is accepted to make use of pressure attempt, however in a lot of the instances you must go on an alternate path with correct error handlers.
Swift errors are usually not exceptions
The Swift compiler at all times requires you to catch all thrown errors, so a scenario of unhandled error won’t ever happen. I am not speaking about empty catch blocks, however unhandled throwing features, so you possibly can’t attempt with out the do-catch companions. That is one key distinction when evaluating to exceptions. Additionally when an error is raised, the execution will simply exit the present scope. Exceptions will often unwind the stack, that may result in reminiscence leaks, however that is not the case with Swift errors. 👍
Introducing the consequence sort
Swift 5 introduces a long-awaited generic consequence sort. Because of this error dealing with might be much more easy, with out including your personal consequence implementation. Let me present you our earlier divide operate through the use of End result.
func divide(_ x: Int, by y: Int) -> End result<Int, DivisionError> {
guard y != 0 else {
return .failure(.zeroDivisor)
}
return .success(x / y)
}
let consequence = divide(10, by: 2)
swap consequence {
case .success(let quantity):
print(quantity)
case .failure(let error):
print(error.localizedDescription)
}
The consequence sort in Swift is mainly a generic enum with a .success and a .failure case. You may go a generic worth in case your name succeeds or an Error if it fails.
One main benefit right here is that the error given again by result’s sort protected. Throwing features can throw any type of errors, however right here you possibly can see from the implementation {that a} DivisionError is coming again if one thing unhealthy occurs. One other profit is that you should use exhaustive swap blocks to “iterate by way of” all of the doable error instances, even and not using a default case. So the compiler can maintain you protected, e.g. if you’re going to introduce a brand new error sort inside your enum declaration.
So through the use of the End result sort it is clear that we’re getting again both consequence information or a strongly typed error. It is not doable to get each or neither of them, however is that this higher than utilizing throwing features? Nicely, let’s get asynchrounous!
func divide(_ x: Int, by y: Int, completion: ((() throws -> Int) -> Void)) {
guard y != 0 else {
completion { throw DivisionError.zeroDivisor }
return
}
completion { return x / y }
}
divide(10, by: 0) { calculate in
do {
let quantity = attempt calculate()
print(quantity)
}
catch {
print(error.localizedDescription)
}
}
Oh, my expensive… an internal closure! A completion handler that accepts a throwing operate, so we will propagate the error thrown to the outer handler? I am out! 🤬
An alternative choice is that we remove the throwing error utterly and use an non-obligatory because of this, however on this case we’re again to sq. one. No underlying error sort.
func divide(_ x: Int, by y: Int, completion: (Int?) -> Void) {
guard y != 0 else {
return completion(nil)
}
completion(x / y)
}
divide(10, by: 0) { consequence in
guard let quantity = consequence else {
fatalError("nil")
}
print(quantity)
}
Lastly we’re getting someplace right here, however this time let’s add our error as a closure parameter as properly. You need to observe that each parameters should be optionals.
func divide(_ x: Int, by y: Int, completion: (Int?, Error?) -> Void) {
guard y != 0 else {
return completion(nil, DivisionError.zeroDivisor)
}
completion(x / y, nil)
}
divide(10, by: 0) { consequence, error in
guard error == nil else {
fatalError(error!.localizedDescription)
}
guard let quantity = consequence else {
fatalError("Empty consequence.")
}
print(quantity)
}
Lastly let’s introduce consequence, so we will remove optionals from our earlier code.
func divide(_ x: Int, by y: Int, completion: (End result<Int, DivisionError>) -> Void) {
guard y != 0 else {
return completion(.failure(.zeroDivisor))
}
completion(.success(x / y))
}
divide(10, by: 0) { consequence in
swap consequence {
case .success(let quantity):
print(quantity)
case .failure(let error):
print(error.localizedDescription)
}
}
See? Strongly typed errors, with out optionals. Dealing with errors in asynchronous operate is manner higher through the use of the End result sort. If you happen to contemplate that a lot of the apps are doing a little type of networking, and the result’s often a JSON response, there you have already got to work with optionals (response, information, error) plus you may have a throwing JSONDecoder methodology… cannot wait the brand new APIs! ❤️
Working with the End result sort in Swift 5
We already know that the consequence sort is mainly an enum with a generic .succes(T)
and a .failure(Error)
instances, however there may be extra that I might like to point out you right here. For instance you possibly can create a consequence sort with a throwing operate like this:
let consequence = End result {
return attempt divide(10, by: 2)
}
It is usually doable to transform again the consequence worth by invoking the get operate.
do {
let quantity = attempt consequence.get()
print(quantity)
}
catch {
print(error.localizedDescription)
}
Additionally there are map
, flatMap
for reworking success values plus it’s also possible to use the mapError
or flatMapError
strategies if you would like to remodel failures. 😎
let consequence = divide(10, by: 2)
let mapSuccess = consequence.map { divide($0, by: 2) }
let flatMapSuccess = consequence.flatMap { divide($0, by: 2) }
let mapFailure = consequence.mapError {
NSError(area: $0.localizedDescription, code: 0, userInfo: nil)
}
let flatMapFailure = consequence.flatMapError {
.failure(NSError(area: $0.localizedDescription, code: 0, userInfo: nil))
}
That is it in regards to the End result sort in Swift 5. As you possibly can see it is extraordinarily highly effective to have a generic implementation constructed immediately into the language. Now that now we have consequence, I simply want for larger kinded sorts or an async / await implementation. 👍