Migrating `if`s to `guard`s in Swift

Scattered throughout my code are uncountable variations on the following statement:

if some condition {continue}

Recently, I have been converting a vast swath of these from if statements to guards. This post describes my reasons for voluntarily performing this migration: a step that seemingly has no impact on the efficiency or correctness of my code.

The Guard Statement

Guard statements are best known for adding conditional bindings to an enclosing scope:

guard let value = possibleNil else {leave scope}

But guards can also differentiate positive and negative execution paths:

guard some condition else {continue}

In this latter form, guard statements act as non-fatal assertions. They prevent forward progress should that precondition fail. It checks a condition and then, if false, leaves scope via continue, break, return, etc. For example, in loops, they limit whether each iteration is fully run. In functions, they prevent bad data from causing errors.

A properly-constructed guard statement takes on the metaphoric characteristics of a human traffic flagman. It allows your code to proceed only when it conforms to certain rules.

Establishing Execution Requirements

A general guard statement establishes requirements for execution. It also provides a safe path for transferring control if and when those requirements fail. Contrast that behavior with assert() and precondition(). Like these other runtime checks, guards add tests for desired state. However, its outcome is not noisy fatal errors that terminate execution.

Guards with requirements are not substitutes for normal if clauses. Avoid guard when a Boolean false value introduces side effects or runs a true execution branch. Instead, guard creates instantly identifiable predicates that establish the required conditions for code to execute properly and offer early exit on failure.

Refactoring

The guard keyword jumps out from code in a way that unambiguously distinguishes these statements from if. Even though my code would continue to operate without my by-hand-migration, upgrading to guard from if improves readability and enhances the clarity of my intent.

Placing guards at the start of scopes, as I do with assertions, forms a new kind of requirements block. Dave Abrahams wrote about the roles of assert and precondition on the Swift Evolution list:

The two functions have distinct roles:
assert: checking your own code for internal errors.
precondition: for checking that your clients have given you valid arguments.

To this, I add guard: establishing a set of requirements that must be met before the code that follows can be safely executed. Consider refactoring any statement that say, in essence, “unless this statement is true, do not progress any further”.

Are you migrating to guard in your own code? And if so, what are the rules you use to decide whether to stay with “if” or switch to “guard”? Let me know in the comments or throw me a tweet or email.

Like the post? Buy a book.

6 Comments

  • Great stuff as always Erica. I was curious to hear more about conditions you feel where guard is inappropriate. In your write up you mention avoiding guard in Bool false values that produce side effects or run a “true” execution branch. (May you please elaborate on what this means precisely?). We’re often lambasted with the virtues of guard but rarely are we told when guard shouldn’t be used. Do you have any other instances that come to mind where guard seems inappropriate (sans trivial if-else implementations and/or complex else statements?

  • Yup, whenever I touch a function, I’ll look and see if I can put a guard at the top. It often results in less if-statements and thus less indents.

    Makes the code more readable because you will often replace an if-statement that had a “negative test”. And negative tests are harder to read:

    if !user.authenticated {
    ….
    }

    Becomes:

    guard user.authenticated else {
    return
    }

  • There are undeniably cases in which guard decreases the mental load on the developer;

        guard let y = possibleNil else {return}
        // do stuff with y
    

    is clearly better than;

        if let y = possibleNil {
            // do stuff with y
        }
    

    But I’m not sure I would spend any time changing this;

    func f(b: Bool) {    
        if !b  {return}
    }
    

    to this;

    func f(b: Bool) {
        guard b else {return}
    }
    

    The positive test bias is, I believe, a triumph of enthusiasm over objectivity. I do, however, heartily welcome the increased focus on early exits that guard has unleashed.

    • That does really tidy up early entry validation in a func

  • I believe the main point to be to follow more readable and compiler-safe coding standards moving forward. The guard statement makes your early exit intention explicit and it is enforced by the compiler. This is not so for if statements.

    The way I look at it is if you are designing by contract and plan to exit early if something is wrong, then use guard. If at any point in your function you don’t intend to exit early then use if.

    I made a post on this awhile back that shows what Apple said about it directly in the docs… https://swift.ijoe.co/swift-guard-statement-8d652c78daa#.9npihuhbi

    • Good point about compiler enforcement.