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
- • Use guard for early exit and to keep the happy path unindented
- • Unwrap optionals with guard let and keep the value in scope
- • Understand why a guard's else block must leave the scope
- • Schedule cleanup with defer
- • Know that multiple defer blocks run in reverse order
- • See that defer runs on every exit path, including thrown errors
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 olderYour 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
// Unknown3️⃣ 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
// ANow 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
// thirdPro Tips
- 💡 Guard for preconditions. Put your "must be true" checks at the very top so the rest of the function reads as the happy path.
- 💡 Defer for cleanup. Acquire a resource, then immediately defer its release — they sit together and can't drift apart.
- 💡 Remember LIFO. The last defer you write is the first to run.
- 💡 guard needs an exit. If the compiler complains, your else isn't leaving the scope.
Common Errors (and the fix)
- "'guard' body must not fall through, consider using a 'return' or 'throw'": your else block didn't leave the scope. Add return, throw, break, or continue.
- Using a guard-let value above the guard: the binding only exists after the guard. Move the usage below it.
- Expecting defer to run code that was never reached: a defer only schedules its block once execution passes that line.
- Surprised by defer order: they run in reverse. Re-read them bottom-to-top.
📋 Quick Reference
| Task | Code | Effect |
|---|---|---|
| Early exit | guard cond else { return } | leave if not met |
| Unwrap + keep | guard let x = opt else { … } | x stays in scope |
| Validate input | guard a, b, c else { … } | all must hold |
| Cleanup | defer { close() } | runs on exit |
| Order | multiple defers | reverse (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
- Previous: Error Handling (do/try/catch)
- Next: Optional Chaining — Safely call into a chain of optionals with a?.b?.c
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.