guard & defer

Reviewed & published by Brayan K

Two small keywords that make Swift code dramatically cleaner: guard exits early when something is wrong so your main logic stays flat, and defer guarantees cleanup runs no matter how a function ends.

Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ guard — Exit Early, Stay Flat

A guard states what must be true to continue. If the condition fails, the else block runs and must leave the current scope (usually return). The big win: any value unwrapped by guard let stays in scope below the guard, so your main logic never gets buried in nested if blocks.

// 'guard' checks a condition and bails out EARLY if it isn't met.
// The else block MUST leave the current scope (return / throw / etc.).

func greet(_ name: String?) {
    // If name is nil, print a message and return — no nesting needed.
    guard let name = name else {
        print("No name given")
        return
    }
    // From here on, 'name' is a plain non-optional String, still in scope.
    print("Hello, \(name)!")
}

greet("Ada")   // Hello, Ada!
greet(nil)     // No name given

// Compare to if let, which nests the happy path:
func greetNested(_ name: String?) {
    if let name = name {
        print("Hi, \(name)!")   // main logic is indented
    } else {
        print("No name given")
    }
}
greetNested("Sam")   // Hi, Sam!

2️⃣ Validating Several Conditions Up Front

Stacking guards is the idiomatic way to validate inputs at the top of a function. Each one peels off a failure case, and every binding remains available afterwards.

// guard shines when you have several things to validate up front.
func makeAccount(name: String?, age: Int?) -> String {
    guard let name = name, !name.isEmpty else {
        return "Error: name is required"
    }
    guard let age = age, age >= 18 else {
        return "Error: must be 18 or older"
    }
    // Both bindings (name, age) are in scope for the rest of the function.
    return "Welcome \(name), age \(age)"
}

print(makeAccount(name: "Ada", age: 30))   // Welcome Ada, age 30
print(makeAccount(name: "", age: 30))       // Error: name is required
print(makeAccount(name: "Sam", age: 15))    // Error: must be 18 or older

Your turn. Fill in the blank to make this function exit early when the name is missing.

// 🎯 YOUR TURN — fill in each ___ then run it on SwiftFiddle.

func firstName(from full: String?) -> String {
    // 1) Use guard to unwrap 'full'; if it's nil, return "Unknown".
    ___ let full = full else {        // 👉 the early-exit keyword
        return "Unknown"
    }
    // full is now a plain String, in scope below.
    let parts = full.split(separator: " ")
    return String(parts.first ?? "")
}

print(firstName(from: "Ada Lovelace"))   // Ada
print(firstName(from: nil))              // Unknown

// ✅ Expected output:
//    Ada
//    Unknown

3️⃣ defer — Cleanup That Always Runs

defer registers a block to run as the current scope exits — on a normal return, an early return, or a thrown error. Multiple defer blocks unwind in reverse order (last declared runs first), which mirrors how you'd tear resources down.

// 'defer' schedules code to run when the current scope EXITS,
// no matter HOW it exits (normal return, early return, or a thrown error).

func process() {
    print("1. start")
    defer { print("4. cleanup (deferred)") }   // runs last, as we leave
    print("2. doing work")
    print("3. about to return")
}

process()
// Output order:
// 1. start
// 2. doing work
// 3. about to return
// 4. cleanup (deferred)

// Multiple defers run in REVERSE order (last declared runs first).
func order() {
    defer { print("A") }
    defer { print("B") }
    defer { print("C") }
    print("body")
}
order()
// body
// C
// B
// A

Now you try. Predict the order, then run it to confirm.

// 🎯 YOUR TURN — predict, then run.

func demo() {
    defer { print("third") }   // 1) which prints LAST?
    defer { print("second") }
    print("first")
}
demo()

// ✅ Expected output (defers run in reverse):
//    first
//    second
//    third

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeEffect
Early exitguard cond else { return }leave if not met
Unwrap + keepguard let x = opt else { … }x stays in scope
Validate inputguard a, b, c else { … }all must hold
Cleanupdefer { close() }runs on exit
Ordermultiple defersreverse (LIFO)

Mini-Challenge: A Safe Divider

No blanks this time — just a brief and an outline. Build it, run it, and confirm the deferred log prints on every exit path.

// 🎯 MINI-CHALLENGE: a safe divider
// 1. Write  func divide(_ a: Int, by b: Int) -> Int?
// 2. Use guard to return nil when b == 0 (avoid a crash).
// 3. Otherwise return a / b.
// 4. Add a defer that prints "divide() finished" so it logs on EVERY exit.
// 5. Test both divide(10, by: 2) and divide(10, by: 0).
//
// ✅ Expected:
//    "divide() finished" prints both times.
//    divide(10, by: 2) -> Optional(5);  divide(10, by: 0) -> nil

// your code here

Practice quiz

What is the main purpose of a guard statement?

  • To loop over a collection
  • To define a new type
  • To exit the current scope early when a condition is not met
  • To pause an async task

Answer: To exit the current scope early when a condition is not met. guard checks a condition and, if it fails, runs its else block which must leave the current scope.

A guard statement's else block must do what?

  • Transfer control out of the current scope (return, break, continue, or throw)
  • Return a value
  • Print an error
  • Call another guard

Answer: Transfer control out of the current scope (return, break, continue, or throw). The else block of guard must exit the scope — via return, break, continue, throw, or fatalError.

Where is a value bound by 'guard let name = optional' available?

  • Only inside the guard's else block
  • Only on the line with the guard
  • Nowhere — it is discarded
  • For the rest of the enclosing scope after the guard

Answer: For the rest of the enclosing scope after the guard. Unlike if let, a guard let binding stays in scope after the guard, which is its key advantage.

When does code inside a defer block run?

  • Immediately when reached
  • When the current scope is about to exit
  • Only if an error is thrown
  • At program startup

Answer: When the current scope is about to exit. defer schedules its block to run as the enclosing scope exits, however it exits.

If a function has two defer blocks, in what order do they run?

  • In reverse order — last declared runs first
  • First declared runs first
  • Both run at the same time
  • Only the first one runs

Answer: In reverse order — last declared runs first. Multiple defer blocks execute in reverse (LIFO) order as the scope exits.

Does a defer block still run if the function returns early via guard?

  • No, defer is skipped on early return
  • Only if the return is at the end
  • Yes, defer runs on any exit from its scope after it was reached
  • Only inside loops

Answer: Yes, defer runs on any exit from its scope after it was reached. defer runs no matter how the scope exits — normal return, early return, or thrown error.

Compared to if let, why is guard let often preferred for validation?

  • It is faster at runtime
  • It keeps the happy path unindented and the unwrapped value in scope
  • It cannot fail
  • It works only with strings

Answer: It keeps the happy path unindented and the unwrapped value in scope. guard let avoids the 'pyramid of doom' — the main logic stays at the top level and the binding persists.

What happens if a defer block is declared but its line is never reached before the scope exits?

  • It still runs
  • It causes a crash
  • It runs twice
  • It does not run — only deferred statements that were reached are scheduled

Answer: It does not run — only deferred statements that were reached are scheduled. A defer only schedules its block once execution reaches the defer statement.

Which keyword would NOT be valid in a guard's else block inside a regular (non-throwing, non-loop) function?

  • return
  • continue
  • fatalError()
  • throw is not allowed in a non-throwing function

Answer: continue. continue and break are only valid inside loops; in a plain function you use return, throw (if throwing), or fatalError().

A common use of defer is to...

  • Allocate memory
  • Speed up loops
  • Guarantee cleanup like closing a file or resource runs no matter how the scope exits
  • Define protocols

Answer: Guarantee cleanup like closing a file or resource runs no matter how the scope exits. defer is ideal for cleanup — closing files, releasing locks — that must happen on every exit path.

Continue this course

Frequently asked questions

What is the difference between guard and if let?

Both can unwrap optionals, but they differ in scope and intent. 'if let' creates a binding that only lives inside the if block, and the happy path is indented. 'guard let' is the reverse: its else block handles the failure and must exit the scope, while the unwrapped value stays in scope for the rest of the function. Use guard for early validation so your main logic stays flat and readable.

What must a guard's else block do?

It must transfer control out of the current scope. In a function that means return (or throw, if the function is marked throws); inside a loop it can be break or continue; and as a last resort fatalError() or a similar never-returning call. The compiler enforces this — if your else block could fall through, it won't compile.

When exactly does a defer block run?

A defer block runs as the enclosing scope is about to exit, after the rest of the scope's work is done. That includes a normal end-of-function, an early return, breaking out of a loop, or an error being thrown. This makes defer perfect for cleanup you want to happen on every exit path — closing a file, releasing a lock, or balancing a counter.

If I have several defer blocks, what order do they run in?

They run in reverse (last-in, first-out) order. The defer declared last runs first as the scope unwinds. This mirrors how nested resources should be torn down: the most recently acquired resource is released first. Note that only defers whose line was actually reached get scheduled.

Does defer run if my function throws an error or returns early?

Yes — that is precisely why defer exists. Once execution has passed a defer statement, its block is guaranteed to run when the scope exits, whether that exit is a normal return, an early guard return, or a thrown error propagating out. It is Swift's reliable cleanup mechanism.