use the end result sort to deal with errors in Swift 5?


From this tutorial you’ll be able to learn to make the most of the do-try-catch syntax with the model new end result sort to deal with errors in Swift.

iOS

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 large milestone occurred in Swift 2, the place Apple utterly revamped error administration. These days you need to use the do, attempt, catch, throw, throws, rethrows key phrases as a substitute 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 most effective practices of error dealing with within the Swift programming lanugage, subsequent I will present you some cool stuff by utilizing outcomes to take care of errors. 🚧

Optionals as error indicators

For easy eventualities you’ll be able to at all times use elective values, to point that one thing dangerous 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 concerning the underlying sort of the error, this strategy is ok, however generally issues can get extra sophisticated, so that you would possibly want some particulars about the issue. Anyway, you’ll be able to at all times cease the execution by calling the fatalError technique, however if you happen to achieve this, nicely… 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 put up, so right here is only a fast cheat sheet of accessible 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 by utilizing the Error protocol

You’ll be able to outline your individual error sorts by merely confirming to the built-in Error protocol. Normally most builders use an enum in an effort to outline completely different causes. You too can have a customized error message if you happen to 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 if you happen to achieve this in a operate, it’s a must 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? {
        change 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’s going to 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 Capabilities and Strategies A operate or technique will be declared with the rethrows key phrase to point that it throws an error provided that certainly one of it’s operate parameters throws an error. These capabilities and strategies are referred to as rethrowing capabilities and rethrowing strategies. Rethrowing capabilities and strategies should have a minimum of one throwing operate parameter.

Okay, so a throwing operate can emit completely different error sorts, additionally it might probably propagate all of the parameter errors, however how can 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 positively room for attempting out issues! Unhealthy 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’ll be able to see the syntax is fairly easy, you’ve got a do block, the place you’ll be able to attempt to execute your throwing capabilities, if one thing goes incorrect, you’ll be able to deal with the errors in numerous catch blocks. By default an error property is offered inside each catch block, so you do not have to outline one your self by hand. You’ll be able to 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 check out!

As we have seen earlier than you’ll be able to merely attempt to name a operate that throws an error inside a do-catch block. If the operate triggers some type of error, you’ll be able to put your error dealing with logic contained in the catch block. That is quite simple & easy.

Typically if you happen to do not actually care concerning the underlying error, you’ll be able to merely convert your throwing operate end result into an elective by utilizing attempt?. With this strategy you may get a zero end result if one thing dangerous occurs, in any other case you may get again your common worth as it’s anticipated. Right here is the instance from above by utilizing attempt?:

guard let quantity = attempt? divide(10, by: 2) else {
    fatalError("This could work!")
}
print(quantity) 

One other approach is to forestall error propagation by utilizing attempt!, however it’s a must to be extraordinarily cautious with this strategy, as a result of if the execution of the “tried operate” fails, your software will merely crash. So use provided that you are completely certain that the operate will not throw an error. ⚠️

let quantity = attempt! divide(10, by: 2) 
print(quantity)

There are a couple of locations the place it is accepted to make use of drive attempt, however in many of the circumstances it is best to go on an alternate path with correct error handlers.

In Swift 5 nested elective attempt? values are going to be flattened to a single elective worth. SE-0230 is an already applied proposal that may break some current Swift code. Paul Hundson has a fast article about this habits.

Swift errors usually are not exceptions

The Swift compiler at all times requires you to catch all thrown errors, so a state of affairs of unhandled error won’t ever happen. I am not speaking about empty catch blocks, however unhandled throwing capabilities, so you’ll be able to’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 end result sort

Swift 5 introduces a long-awaited generic end result sort. Which means error dealing with will be much more easy, with out including your individual end result implementation. Let me present you our earlier divide operate by utilizing 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 end result = divide(10, by: 2)
change end result {
case .success(let quantity):
    print(quantity)
case .failure(let error):
    print(error.localizedDescription)
}

The end result sort in Swift is principally a generic enum with a .success and a .failure case. You’ll be able to 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 secure. Throwing capabilities can throw any type of errors, however right here you’ll be able to see from the implementation {that a} DivisionError is coming again if one thing dangerous occurs. One other profit is that you need to use exhaustive change blocks to “iterate by means of” all of the potential error circumstances, even and not using a default case. So the compiler can maintain you secure, eg. if you will introduce a brand new error sort inside your enum declaration.

So by utilizing the End result sort it is clear that we’re getting again both end result knowledge or a strongly typed error. It is not potential to get each or neither of them, however is that this higher than utilizing throwing capabilities? Properly, 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 pricey… an internal closure! A completion handler that accepts a throwing operate, so we are able to propagate the error thrown to the outer hander? I am out! 🤬

An alternative choice is that we remove the throwing error utterly and use an elective 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) { end result in
    guard let quantity = end result 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 nicely. You must notice that each parameters must 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) { end result, error in
    guard error == nil else {
        fatalError(error!.localizedDescription)
    }
    guard let quantity = end result else {
        fatalError("Empty end result.")
    }
    print(quantity)
}

Lastly let’s introduce end result, so we are able to 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) { end result in
    change end result {
    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 approach higher by utilizing the End result sort. If you happen to think about that many of the apps are performing some type of networking, and the result’s often a JSON response, there you have already got to work with optionals (response, knowledge, error) plus you’ve got a throwing JSONDecoder technique… cannot wait the brand new APIs! ❤️

Working with the End result sort in Swift 5

We already know that the end result sort is principally an enum with a generic .succes(T) and a .failure(Error) circumstances, however there may be extra that I might like to indicate you right here. For instance you’ll be able to create a end result sort with a throwing operate like this:

let end result = End result {
    return attempt divide(10, by: 2)
}

Additionally it is potential to transform again the end result worth by invoking the get operate.

do {
    let quantity = attempt end result.get()
    print(quantity)
}
catch {
    print(error.localizedDescription)
}

Additionally there are map, flatMap for remodeling success values plus you may as well use the mapError or flatMapError strategies if you would like to rework failures. 😎

let end result = divide(10, by: 2) 
let mapSuccess = end result.map { divide($0, by: 2) } 
let flatMapSuccess = end result.flatMap { divide($0, by: 2) } 
let mapFailure = end result.mapError { NSError(area: $0.localizedDescription, code: 0, userInfo: nil) }
let flatMapFailure = end result.flatMapError { .failure(NSError(area: $0.localizedDescription, code: 0, userInfo: nil)) }

That is it concerning the End result sort in Swift 5. As you’ll be able to see it is extraordinarily highly effective to have a generic implementation constructed instantly into the language. Now that we’ve end result, I simply want for increased kinded sorts or an async / await implementation. 👍