radex.io

aboutarchiveetsybooksmastodontwitter

When (not) to use guard

December 14, 2015
Hey! This is an old post. It reflects my views and the state of the world in 2015, but is likely to be outdated. You’ll find me on Twitter, building Nozbe, and making engineering blueprints.

guard is one of my favorite features of Swift 2. It’s one of those subtle syntactical constructs we could totally do without. And yet having it is such a delightful win. It makes our methods cleaner and easier to read, it helps express the “early exit” intention, and adds a little extra safety.

However, it’s important to learn and understand how to use guard properly. It has its place, but it’s not meant to replace if..else and if let in all cases. No matter how great guard is, it can easily be misapplied and forced into places where other constructs do a better job.

Here are some basic guidelines for when to guard, and when not to:

Do: verify entry conditions

This is perhaps the simplest and most common use case for guard. You have a function that does a specific job, but only when one or a number of basic conditions are met at the beginning.

For example:

func updateWatchApplicationContext() {
    let session = WCSession.defaultSession()
    guard session.watchAppInstalled else { return }

    do {
        let context = ["token": api.token]
        try session.updateApplicationContext(context)
    } catch {
        print(error)
    }
}

This has two advantages:

First of all, by putting the check at the top of the function, instead of wrapping the whole method in an if, we’re conveying that the condition isn’t part of the fundamental job the function does. Instead, it’s just a basic entry condition.

Second of all, using guard suggests to the reader, and ensures at compile time, that if the condition is false, the method will return. The compiler check, albeit subtle, helps with long-term maintainability of the code — if someone accidentally removes the early exit from the else clause, the resulting bug will be caught immediately.

Do: early exits on a long success path

Use case: a function with a relatively long “happy path”, but with one or more checks that will be met in the best case scenario, but should just return or throw an error if false.

func vendAllNamed(itemName: String) throws {
    guard isEnabled else {
        throw VendingMachineError.Disabled
    }

    let items = getItemsNamed(itemName)

    guard items.count > 0 else {
        throw VendingMachineError.OutOfStock
    }

    let totalPrice = items.reduce(0, combine: +)

    guard coinsDeposited >= totalPrice else {
        throw VendingMachineError.InsufficientFunds
    }

    coinsDeposited -= totalPrice
    removeFromInventory(itemName)
    dispenseSnacks(items)
}

Do: unwrap optionals (flatten if let..else pyramids)

Similar scenario: you need an entry condition, or checks scattered between code on a long happy path. But instead of boolean conditions, you want to verify that some optionals have a value and unwrap them:

func taskFromJSONResponse(jsonData: NSData) throws -> Task {
    guard let json = decodeJSON(jsonData) as? [String: AnyObject] else {
        throw ParsingError.InvalidJSON
    }

    guard let id = json["id"] as? Int,
          let name = json["name"] as? String,
          let userId = json["user_id"] as? Int,
          let position = json["pos"] as? Double
    else {
        throw ParsingError.MissingData
    }

    return Task(id: id, name: name, userId: userId, position: position)
}

Pro-Tip: there are better ways of dealing with JSON in Swift.

Unwrapping optionals is where guard really shines. Unlike if let, which unwraps the value for use in between if’s curly braces, guard adds unwrapped values to the outer scope — so you can just use them after guard, not inside.

The reason why this is great is because if you have more than one instance of optional unwrapping, you can avoid the pyramid of doom (multiple levels of nested if lets).

In non-trivial cases, it’s easier for our brains to follow a flat path with a few early exits rather than analyze nested branches.

Do: return and throw

As a general rule, an early exit means one of three things:

Execution was interrupted

When your method returns no value, merely executes a command, but it cannot be completed.

Example: a function that updates WatchKit application context, but the iOS app is not connected to any Apple Watch.

Do: just return

The result of a computation is an empty value

When your method returns something, possibly by transforming the input parameter, and the transformation can’t be carried out completely.

Example: a function that returns an array of objects deserialized from a cache, but there’s no cache saved on disk.

Do:

The computation failed with an error

When your method can fail in more than one non-trivial way and you want to communicate to the caller the reason for failure.

Example: a method that reads a file from disk, or a method that takes a network response and parses it.

Do:

Do: log, crash, and assert

Logging

Sometimes it makes sense to log a diagnostic message to the console before returning early, at least during development. This helps you track failure states, even if your code handles them correctly. However, it’s rarely appropriate to have more code in guard’s else block than that.

Fatal conditions

If a failure to fulfill a condition is a serious programming error, it might make sense to purposefully crash your program. If your app will crash either way, or will end up in an illegal state, it’s usually better to do it yourself. This way, you can reliably exit in a known location and display a reason for the crash.

Usually, the way to do it is using precondition:

precondition(internet.kittenCount == Int.max, "Not enough kittens in the internet")

However, if the condition involves unwrapping optionals, not just a simple boolean expression, use guard:

guard let kittens = internet.kittens else {
    fatalError("OMG ran out of kittens!")
}

Assertions

Sometimes, a condition is always expected to be fulfilled, but a failure to do so isn’t a serious programming error. In that case, consider using assertionFailure like so:

guard let puppies = internet.puppies else {
    assertionFailure("Huh, no dogs")
    return nil
}

This way, you’ll crash and easily catch a bug during development and internal testing, but in production, the app won’t crash (even if it will be buggy).

Again, if the condition is purely boolean, assert(condition) will usually do the job.

Don’t: use guard for trivial if..else expressions

When you have a trivial method that only contains a single, simple if..else expression, don’t use guard.

// Don't:
var projectName: String? {
    guard let project = task.project where project.isValid else {
        return nil
    }

    return project.name
}

For such simple cases, it’s much easier to understand and conceptualize a two-branch if..else expression rather than the flattened version — despite otherwise being a good candidate for a guard.

// Better!
var projectName: String? {
    if let project = task.project where project.isValid {
        return project.name
    } else {
        return nil
    }
}

Pro-Tip: Make sure you understand optional chaining, Optional.map, and Optional.flatMap — you can often avoid having an explicit if let altogether with those tools.

Don’t: use guard as a “reverse if”

Some languages, such as Ruby, have an unless statement, which is essentially a “reverse if” — the code inside the statement is executed when passed condition evaluates to false.

Swift’s guard, despite a few similarities, is not the same thing. guard is not a general-purpose branching mechanism. It’s specifically meant to be used for early exits when expected conditions fail.

Even in situations where you can bend guard to act as a reverse if, don’t. Just use if..else or consider splitting your code into multiple functions.

// Don't:
guard let s = sequence as? Set<Element> else {
    for item in sequence {
        insert(item)
    }
    return
}

switch (s._variantStorage) {
case .Native(let owner):
    _variantStorage = .Native(owner)
case .Cocoa(let owner):
    _variantStorage = .Cocoa(owner)
}

Don’t: put complex code in else clause

This is a corollary of the previous rule:

guard’s else clause shouldn’t be more than a simple early exit. It’s OK to make diagnostic logs, but rarely anything else. It’s also fine to clean up any unfinished work or opened resources, although most of the time you should use defer for that.

But essentially, if you do any real work in the else block, aside from what’s needed to just get out of the function, you’re misusing guard.

Rule of thumb: guard’s else clause should almost never have more than two or three lines of code.

Published December 14, 2015. Send feedback.