The Swift defer keyword lets you schedule a block of code to run right before the current scope exits, no matter how it exits. Whether your function returns normally, returns early because of a guard statement, or throws an error, the code inside your defer block will always run.
Think of it like setting up a “cleanup crew” the moment you open something. You hire them right away, but they do their work on the way out. That way, you never have to remember to call them yourself.
In this article, you’ll learn exactly how defer works in Swift, walk through the most common ways to use it, see how it shows up in real iOS code, and understand the mistakes that trip up beginners. By the end, you’ll know when to reach for it and when to leave it alone.
The Basic Syntax
Here is the simplest possible use of defer in Swift. Read through it, then look at what gets printed and in what order:
func makeBreakfast() {
defer {
print("Washed the dishes")
}
print("Made coffee")
print("Made toast")
}
makeBreakfast()
// Output:
// Made coffee
// Made toast
// Washed the dishesEven though “Washed the dishes” is written first in the function, it prints last. That’s defer in action. Let’s break down each part:
| Line | What it does |
|---|---|
defer { } | Registers the code inside the curly braces to run later, right before this function exits. The code isn’t run now — it’s scheduled. |
print("Washed the dishes") | This is the deferred work. It will run last, after everything else in the function has finished. |
print("Made coffee") | This runs immediately when encountered, as normal. No deferring here. |
print("Made toast") | Also runs immediately. At this point the function is about to end, so Swift now runs the deferred block. |
Breaking It Down
What “scope” means here
A scope is just the area between a pair of curly braces. A function body is a scope. An if statement is a scope. A for loop is a scope. When Swift sees a defer block, it waits until that surrounding scope is finished, then runs the deferred code.
For most beginners, the scope you care about is the function. You write defer near the top of a function, and it runs right as the function is about to return.
defer runs on every exit, not just the normal one
This is the key insight. Your function might have three different guard statements that can return early. Without defer, you’d need to put your cleanup code in all three places. With defer, you write it once and it runs no matter which exit path is taken.
Does Swift defer execute on return?
Yes. The deferred code runs after the return value is prepared but before the function actually hands control back to the caller. This means a defer block cannot change the value a function returns, but it will always run before the return completes.
Multiple defers run in reverse order
If you write multiple defer blocks in the same scope, Swift runs them last-in, first-out. Think of it like a stack of plates: the last one you put on top is the first one you take off.
func stackExample() {
defer { print("First defer") } // runs third
defer { print("Second defer") } // runs second
defer { print("Third defer") } // runs first
print("Regular code") // runs first of all
}
// Output: Regular code, Third defer, Second defer, First deferWhat is the difference between defer and deinit in Swift?
defer runs at the end of a scope (usually a function call). deinit runs when a class instance is destroyed and removed from memory. They are completely separate tools. Use defer for function-level cleanup; use deinit for object-level cleanup.
Variations and Syntax Patterns
Here are the most important ways you’ll see and use defer in Swift code. Each one builds on the same idea but applies it in a different context.
func processUser(id: String?) {
defer {
print("Processing finished") // runs no matter what
}
guard let userId = id else {
print("No user ID provided")
return // defer still runs here
}
guard !userId.isEmpty else {
print("User ID is empty")
return // and here
}
print("Processing user: \(userId)")
} // and at normal exit toodefer at the top of the function, then use as many guard statements as you need. No matter which guard triggers, the deferred code always runs.func saveData() throws {
print("Opening connection")
defer {
print("Closing connection") // runs even if an error is thrown
}
let result = try fetchDataFromServer()
print("Saving: \(result)")
}throws, a thrown error exits the scope immediately. Without defer, your cleanup code below the try line would never run. With defer, it runs even when an error is thrown — equivalent to try/finally in languages like Java or Python.for item in ["apple", "banana", "cherry"] {
defer {
print("Done with \(item)") // runs at end of each iteration
}
print("Processing \(item)")
}
// Processing apple / Done with apple
// Processing banana / Done with banana
// Processing cherry / Done with cherrydefer runs at the end of each iteration rather than at the end of the whole loop. This is useful when processing a list of items that each need cleanup, like closing a connection for each request.func processItems() {
do {
defer {
print("Cleaned up the temporary buffer")
}
print("Working with temporary data...")
} // defer runs here, at the end of the do block
print("Back in the main function")
}
// Working with temporary data...
// Cleaned up the temporary buffer
// Back in the main functiondo { } block to create a temporary scope inside a function. Any defer inside it only runs when that do block ends, not when the function ends. This is handy when you want very precise control over when cleanup happens.func captureDemo() {
var status = "starting"
defer {
print("Final status: \(status)") // sees the updated value
}
status = "completed"
print("Did the work")
}
// Did the work
// Final status: completeddefer runs, it sees the variable’s value at that moment, not the value the variable had when you wrote the defer block. This is useful for logging the final state of something before your function exits.func loadUserProfile() {
isLoading = true
defer {
isLoading = false // always hides the spinner
}
guard let userId = getCurrentUserId() else { return }
guard let profile = fetchProfile(for: userId) else { return }
displayProfile(profile)
}defer to turn off a loading spinner. Without it, every early return would need its own isLoading = false line, and it’s easy to miss one. With defer, it’s impossible to forget.How swift defer Appears in Real iOS Code
Let’s look at a realistic iOS example. This is a view model method that fetches a list of posts from an API. It uses defer to guarantee the loading state is always cleaned up, and wraps things in proper error handling.
import SwiftUI
class PostsViewModel: ObservableObject {
@Published var posts: [Post] = []
@Published var isLoading = false
@Published var errorMessage: String?
func fetchPosts() async {
isLoading = true
// This guarantee means we NEVER leave the spinner on by accident.
// Every early return, every thrown error, every success path
// will all hit this defer block before exiting.
defer {
isLoading = false
}
guard let url = URL(string: "https://api.example.com/posts") else {
errorMessage = "Invalid URL"
return // defer runs here
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
let decoded = try JSONDecoder().decode([Post].self, from: data)
posts = decoded
} catch {
errorMessage = error.localizedDescription
}
// defer runs here at the end of the function normally too
}
}Here’s why each decision was made:
Setting isLoading = true before the defer is intentional. The spinner turns on immediately. The defer turns it back off at the very end, no matter what path the function takes.
The guard statement for the URL can return early if the URL is invalid. Without defer, you’d need to add isLoading = false inside that guard’s else block. With defer, you don’t.
The do/catch block handles any errors from the network request or JSON decoding. Whether it throws or succeeds, the defer block still fires on the way out.
This is the kind of code you’ll write regularly in real iOS apps. Any time you have a boolean flag that needs to be reset, or a resource that needs to be released, and there are multiple ways the function might exit, defer is the clean solution.
When Should I Use defer in Swift?
Defer is not something you’ll need in every function. But it’s exactly right in several common situations. Here’s a quick map of when it earns its place:
- 🔒
Locking and unlocking
If you lock something at the start of a function (like a thread lock or a database write lock), defer ensures you unlock it before returning. Forgetting to unlock is a classic source of freezing apps and deadlocks.
- 📂
Opening and closing files or connections
Whenever you open something that needs to be closed — a file handle, a database connection, a network session — put the close call in a defer right after the open. That way they always stay paired together.
- ⏳
Loading states and spinners
Whenever you show a loading indicator at the start of a function, defer is the cleanest way to make sure it always gets hidden. No matter how the function exits, the spinner turns off.
- 🪵
Logging and analytics
Sometimes you want to log how a function ended regardless of the outcome. Putting the log call in a defer means it always fires, and because defer captures variables by reference, it can even log the final state of your data.
defer adds complexity without any benefit. Keep it for situations where cleanup must happen regardless of how the function exits. Overusing it can make code harder to read.Common Mistakes to Avoid
Mistake 1: Expecting defer to change the return value
You might assume that if you modify a variable inside a defer block, that modification will change what the function returns. It won’t.
func getScore() -> Int {
var score = 10
defer {
score = 99 // this does NOT change the return value
}
return score // returns 10, not 99
}
print(getScore()) // prints 10The return value is captured when the return statement is reached. The defer runs after that, so any changes it makes to the variable are too late to affect what gets returned.
Mistake 2: Using return inside a defer block
You can’t use return, break, or continue inside a defer block. Swift won’t let you exit the current scope from inside the block that’s supposed to run during that exit. It would create a logical paradox. Xcode will give you a compile error if you try.
Mistake 3: Thinking defer runs at the point where you write it
New Swift developers sometimes read code with defer and think the deferred code runs right there, in order with everything else. It doesn’t. The deferred code is registered when that line is hit, but it doesn’t execute until the scope is exiting.
func confusingOrder() {
defer { print("B") } // registered first, runs last
print("A") // runs immediately
}
// prints: A, then BThis is the concept that takes most beginners the longest to internalize. The way to think about it: defer is a promise, not an action. You’re promising Swift “do this before leaving.” The promise is made when you write it, but fulfilled when you exit.
Mistake 4: Writing complex logic inside defer
Defer blocks should be short and focused. If you find yourself writing ten lines of business logic inside a defer, that’s a sign something has gone wrong. A defer block that’s hard to read defeats the purpose of using it. Keep deferred code to a single action or a short cleanup sequence at most.
Quick Reference
Here’s a scannable summary of everything you’ve learned about defer in Swift:
| Syntax / Pattern | What It Does |
|---|---|
| defer { } | Schedules the code inside to run right before the current scope exits |
| defer inside a function | Runs before the function returns, regardless of which return or throw is hit |
| defer with guard | Runs even if a guard triggers an early return — the most common real-world pattern |
| defer with throws | Runs even if an error is thrown, equivalent to try/finally in other languages |
| Multiple defers in one scope | Run in reverse order (LIFO) — last written, first executed |
| defer inside a loop | Runs at the end of each loop iteration, not just at the end of the loop |
| defer inside a do block | Runs when the do block ends, not when the function ends |
| Variable capture in defer | Sees the variable’s value at exit time, not at the time defer was written |
| Cannot return inside defer | You can’t use return, break, or continue inside a defer block |
| Cannot change return value | Modifying a variable in defer does not change what the function returns |
Using AI to Go Deeper with swift defer
AI coding assistants like Claude, Copilot, and ChatGPT are part of how modern developers work. Knowing how to prompt them well is itself a skill. Here are three categories of prompts designed to help you actually understand defer, not just copy code that uses it.
Summary
Here’s a quick recap of what you learned about Swift defer:
- The
deferkeyword schedules code to run right before the current scope exits, no matter how it exits - It’s perfect for cleanup tasks like resetting loading states, closing connections, and unlocking resources
- Defer is especially useful when a function has multiple guard statements with early returns
- Multiple defer blocks in the same scope run in reverse order (last written, first executed)
- Defer captures variables by reference, so it sees the variable’s final value at exit time
- You cannot use return, break, or continue inside a defer block
- Modifying a variable inside defer does not change the function’s return value
- Keep defer blocks short and focused on a single cleanup task
The best way to get comfortable with defer is to try it in your own code. Find a function you’ve written that has multiple guard statements and cleanup code repeated in several places. Refactor it to use defer and watch how much cleaner it gets.
Once you’re comfortable with defer, a great next step is going deeper on Swift error handling with do/catch and throws. The two concepts go hand in hand in real iOS apps, and defer is one of the most useful tools for writing clean throwing functions.

