|

guard: Keep the Main Path Flat

Molecules of Swift, episode #003

Swift functions often start with requirements: a value must exist, a string must not be empty, or a request must be valid. Nested if statements can describe these checks, but they can also push the main logic farther and farther to the right.

guard gives those checks a different shape. It states what must be true for the current scope to continue and requires the failed path to leave that scope.

func normalizedUsername(_ input: String?) -> String? {
    guard let input else { return nil }

    let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines)
    guard !trimmed.isEmpty else { return nil }

    return trimmed.lowercased()
}

The value here is not saving braces. guard separates requirements from the main path.

The else branch cannot just continue

A guard statement always has an else branch. If the condition fails, that branch must transfer control out of the guard statement’s enclosing scope. In a function, that often means return or throw. Inside a loop, continue or break can do the job. Calling a function that returns Never, such as fatalError, also satisfies this rule.

That compiler rule is what makes the code after guard useful: if execution reaches the next line, Swift knows the condition succeeded.

func greet(_ name: String?) {
    guard let name else { return }
    print("Hello, \(name)")
}

After the guard, name is a String, not a String?. The binding is available in the function’s outer scope, so the rest of the code can use it directly.

With if let, the unwrapped binding normally stays inside the if branch. That is an important difference: if creates a branch, while guard establishes a requirement for the code that follows.

Keep the failed path next to the requirement

A single guard can combine several conditions:

func endpoint(host: String?, port: Int?) -> String? {
    guard
        let host,
        !host.isEmpty,
        let port,
        (1...65_535).contains(port)
    else {
        return nil
    }

    return "\(host):\(port)"
}

If all conditions describe one requirement — “we have a usable endpoint” — this can read naturally. Combining checks is not always better, though. If different failures need different responses, separate guard statements can make each exit reason clearer.

The goal is not to use as few guard statements as possible. The goal is to keep each requirement understandable.

guard in loops

Leaving the current scope does not always mean ending a function. Inside a loop, continue can end the current iteration:

let names: [String?] = ["Mira", nil, "", "Noah"]

for name in names {
    guard let name, !name.isEmpty else {
        continue
    }

    print(name)
}

The loop body now describes the valid case. Missing and empty values leave the current iteration immediately. This is useful when an iteration has several simple reasons to do nothing: the main work stays at one indentation level.

guard does not replace if

guard works well when a failed condition means the current scope should not continue its normal work.

If both outcomes are normal branches and execution continues afterward, if is usually more precise.

if isAdmin {
    showAdminTools()
} else {
    showStandardTools()
}

refreshScreen()

Neither branch is a failure here. They are simply two valid paths.

A large else block in a guard can also hide the main idea. Early exits work best when the failed path is short enough to understand quickly.

Check and bind at the same time

guard is especially useful because its condition list can combine Boolean checks with optional binding.

guard
    let token = request.token,
    !token.isEmpty,
    request.isEnabled
else {
    return
}

// token is available here as String

One statement rejects a state the main logic cannot use and also unwraps the value needed by the code that follows. The compiler requires the failed path to transfer control and keeps successful bindings available in the outer scope.

Where it works best

guard is a good fit when a function or loop has a clear main path and several conditions that make continuing impossible or pointless. Parsing data, validating requests, checking optional dependencies, filtering loop iterations, and checking requirements before an operation are common examples.

Not every if should become a guard. If a condition describes a normal choice, both branches matter equally, or the failed path takes most of the function, regular branching can be easier to follow.

A useful question is simple: is this one possible path, or is it a requirement without which this work cannot continue? guard is designed for the second case.

Play with it

The companion Molecule #003 contains optional binding, several conditions in one guard, loop filtering with continue, and manual compiler experiments.

Keep the main path visible

guard does not improve code just because it moves a check upward. Its value is the contract: if execution continues, the requirement has already been met.

Reject what cannot continue, unwrap the values you need, and then write the main logic without another level of nesting.

Sources