Upcoming and OnDemand Webinars View full list

Throws Helps Readability

Jeremy Sherman

Swift’s error-related syntax calls attention to possible errors through try and throws. The do/catch syntax clearly separates the happy path (no errors) from the sad path (errors):

func exampleSyncUsageOfThrows() -> Bool {
    do {
        /* happy path */
        let cookie = try ezbake()
        eat(cookie)
        return true
    } catch {
        /* sad path */
        return false
    }
}

Because throws is “viral”, you’re forced to address it one way or another, even if it’s by deciding to flip your lid when you hit an error by using the exploding try!.

No Async Syntax

Swift’s error-related syntax is great when every line of code executes one after another, synchronously. But it all goes to heck when you want to pause between steps to wait for an external event, like a timer finishing or a web server getting back to you with a response, or anything else happening asynchronously.

The Cocoa Completion Callback Pattern: Everything Is Optional

Let’s try that example again, only scheduling the cookie-baking for later, and then waiting for the cookie to cool before scarfing it:

func exampleAsyncDoesNotPlayNiceWithThrows(completion hadDinner: @escaping (Bool) -> Void) {
    ezbakeTomorrow { cookie, error in
        // hope you don't forget to check for an error first!
        // also hope you like optional unwrapping
        guard error == nil, let cookie = cookie else {
            return hadDinner(false)
        }

        wait(tillCool: cookie) { coolCookie, error in
            guard error == nil, let coolCookie = coolCookie else {
                // dog snarfed cookie?
                return hadDinner(false)
            }

            eat(coolCookie)
            hadDinner(true)
        }
    }
}

This approach of calling a completion closure with parameters for both the desired result and the failure explanation all marked optional is common across Cocoa APIs as well as third-party code. Correctly unpacking those arguments relies heavily on convention. That is to say, it relies heavily on you being very careful not to shoot yourself in the foot.

Everything Is Optional?!

Because both the success value (cookie) and the failure value (error) might not be present, both end up being optionals. That means you end up with four cases to consider, of which two should probably never happen:

  • Success! cookie but no error. This is unambiguous.
  • Failure. error but no cookie. This is similarly unambiguous.
  • Kind of a failure, I think? Like, maybe? Both error AND cookie. If you’re following classic Cocoa style, this gets lumped in with the success case, so that a successful run could, before ARC, leave error pointing at fabulously uninitialized data or scratch errors that didn’t happen. (As you might imagine, that convention gets messed up pretty often.)
  • Super-duper extra-failure. Neither error nor cookie. This is probably a bug in whatever’s giving you this output. But, alas, you still have to deal with it as a possibility.

Result: Better Async Support Through Types

Result is a popular enumeration for cleaning this up. It looks something like:

enum Result<Value> {
  case success(Value)
  case failure(Error)
}

This addresses all the weirdness with the conventional approach:

  • It explicitly has only two cases, so you don’t have to waste time considering the two “these are probably a bug” cases.
  • You can’t mess up the convention and shoot yourself in the foot. You can only get access to either the failure or the success case by design.
  • You can’t just ignore the error case and accidentally code for just the happy path. case exhaustiveness ensures the error is on your radar.

Just as do/catch lets you clearly separate handling a successful result from a failure, so does Result through switch/case:

func exampleAsyncLikesResult(completion hadDinner: @escaping (Bool) -> Void) {
    ezbakeTomorrow { result in
        switch result {
        case let .success(cookie):
            wait(tillCool: cookie) { result in
                switch result {
                // look ma, no optionals!
                case let .success(coolCookie):
                    eat(coolCookie)
                    hadDinner(true)

                case let .failure(_):
                    hadDinner(false)
                }
            }

        case let .failure(_):
            hadDinner(false)
        }
    }
}

Result: Sync, Async, It Just Works?

Result achieves the aims of do/catch/throw for async code. But it can also be used for sync code. This leads to competition between Result and throws for the synchronous case:

func exampleSyncUsageofResult() {
    return
        ezbake()
        .map({ eat($0) })
        .isSuccess
}

That’s…not so pretty. It would get even uglier if there was a sequence of possibly failing steps:

// this mess…
func exampleUglierSyncResult() {
    return
        open("some file")
        .flatMap({ write("some text", to: $0) })
        .map({ print("success!"); return $0 })
        .flatMap({ close($0) })
        .isSuccess
}

// …translates directly to this less-mess
func exampleSyncIsLessUglyWithTry() {
    do {
        let file = try open("some file")
        let stillAFile = try write("some text", to: file)
        print("success!")
        try close(stillAFile)
        return true
    } catch {
        return false
    }
}

It’s kind of easy to lose the flow in all that syntax, plus it sounds like you have a funky verbal tic with the repeated map and flatMap. You also have to keep deciding between (and distracting your reader with the distinction between) map and flatMap.

Leave Result for Async, Switch to Throws When Sync

That suggests a rule of thumb: stick with throws for synchronous code. Applying that even to mixed sync (within the body of completion callbacks) and async (did I mention completion callbacks?) code allows to play to the strengths of both throws and Result.

First, here’s a mechanical translation of the earlier exampleAsyncLikesResult function:

func exampleMechanicallyBridgingBetweenAsyncAndSync(completion hadDinner: @escaping (Bool) -> Void) {
    ezbakeTomorrow { result in
        do {
            let cookie = try result.unwrap()
            wait(tillCool: cookie) { result in
                do {
                    let coolCookie = try result.unwrap()
                    eat(coolCookie)
                    hadDinner(true)
                } catch {
                    hadDinner(false)
                }
            }
        } catch {
            hadDinner(false)
        }
    }
}

Each completion accepts a Result, but in working with it, it immediately returns to using the Swift try/throw/do/catch syntax.

try has a try? variant that allows to clean this up even more nicely. This is more like the code you’d likely write in the first place when using this style:

func exampleNicerBridgingBetweenAsyncAndSync(completion hadDinner: @escaping (Bool) -> Void) {
    ezbakeTomorrow { result in
        guard let cookie = try? result.unwrap()
            else { return hadDinner(false) }

        wait(tillCool: cookie) { result in
            guard let coolCookie = try? result.unwrap
                else { return hadDinner(false) }

            eat(coolCookie)
            hadDinner(true)
        }
    }
}

Bridging Helpers

This relies on some simple helper functions to bridge between Result and throws.

  • Result.unwrap() throws goes from Result to throws: The caller of an async method that delivers a Result can then use result.unwrap() to bridge back from Result into something you can try and catch. unwrap()is a throwing function that throws if it’s .failure and otherwise just returns its .success value. We saw plenty of examples earlier.

  • static Result.of(trying:) goes from throws to Result: The implementation of async methods can use Result.of(trying:) to wrap up the result of running a throwing closure as a Result; this helper runs its throwing closure and stuffs any caught error in .failure, and otherwise, wraps the result up in .success.

    This is used to implement async functions delivering a result. Since the running example delivered a Boolean, you haven’t seen this used yet. Here’s a small example:

func youComplete(me completion: @escaping (Result<MissingPiece>) -> Void) {
    doSomethingAsync { boxOfPieces: Result<PieceBox> in
        let result = Result.of {
            let box = try boxOfPieces.unwrap()
            let piece = try findMissingPiece(in: box)
            return piece
        }
        completion(result)
    }
}

What these functions are called varies across Result implementations (I’m eagerly awaiting the Swift version of what Promises/A+ delivered for JavaScript), but whatever your Result calls them, use them! (And if they aren’t there, you can readily write your own.)

For a concrete example of implementing these, as well as the variation in names, check out antitypical/Result’s versions:

That’s a Wrap

So that’s the bottom line:

  • Use Result as your completion callback argument.
  • Within the completion body (and anywhere else you’re working synchronously), use do/catch to work with potential errors.

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project