Closures in Swift: From Syntax to Real-World Usage
In WALL-E, the little robot carries a single directive — a self-contained instruction that bundles both a task and all the context it needs to complete it. Swift closures work the same way: they’re portable blocks of code that capture their surrounding environment and carry it along wherever they go.
In this guide, you’ll learn what closures are, how their syntax works (from verbose to ultra-short), how to use trailing
closures, how closures capture values, and the difference between @escaping and non-escaping closures. We won’t cover
higher-order functions in depth — they have their own dedicated post.
What You’ll Learn
- What Is a Closure?
- Closure Syntax
- Trailing Closures
- Capturing Values
- @escaping vs Non-Escaping
- Common Mistakes
- What’s Next?
What Is a Closure?
A closure is a self-contained block of code that you can store in a variable, pass to a function, or return from a function — just like any other value. If functions are named recipes that Remy follows in the kitchen, closures are sticky notes with quick instructions that anyone can pick up and follow.
Think of WALL-E’s directive chip. It doesn’t just hold an instruction — it also remembers the environment it came from. A closure works the same way: it “closes over” the variables and constants from its surrounding scope and carries them along, even after that scope has ended.
There are three kinds of closures in Swift:
- Global functions — named closures that don’t capture any values.
- Nested functions — named closures that capture values from their enclosing function.
- Closure expressions — unnamed (anonymous) closures written in a lightweight syntax.
This post focuses on closure expressions — the kind you’ll use most often.
Apple Docs:
Closures— The Swift Programming Language
Closure Syntax
The full closure syntax looks like this:
{ (parameters) -> ReturnType in
// body
}
Let’s see it in action. Imagine you have an array of Pixar character names and you want to sort them:
let characters = ["Buzz", "Woody", "Rex", "Jessie"]
let sorted = characters.sorted(by: { (a: String, b: String) -> Bool in
return a < b
})
print(sorted)
["Buzz", "Jessie", "Rex", "Woody"]
That’s the full form. But Swift lets you simplify it step by step.
Step 1: Infer Types
Swift already knows sorted(by:) expects (String, String) -> Bool, so you can drop the type annotations:
let sorted1 = characters.sorted(by: { a, b in
return a < b
})
print(sorted1)
["Buzz", "Jessie", "Rex", "Woody"]
Step 2: Implicit Return
When the closure body is a single expression, you can omit return:
let sorted2 = characters.sorted(by: { a, b in a < b })
print(sorted2)
["Buzz", "Jessie", "Rex", "Woody"]
Step 3: Shorthand Argument Names
Swift provides automatic argument names — $0 for the first argument, $1 for the second, and so on. This lets you
drop the parameter list entirely:
let sorted3 = characters.sorted(by: { $0 < $1 })
print(sorted3)
["Buzz", "Jessie", "Rex", "Woody"]
Tip: The shorthand
$0,$1syntax is great for simple closures. For anything longer than one line, use named parameters for clarity.
Trailing Closures
When the last parameter of a function is a closure, you can write it after the parentheses. This is called trailing closure syntax, and it’s how most Swift APIs are written:
let sorted4 = characters.sorted { $0 < $1 }
print(sorted4)
["Buzz", "Jessie", "Rex", "Woody"]
Trailing closures really shine with map, filter, and similar functions. Here’s map transforming an array of Pixar
movie titles into uppercase:
let movies = ["Up", "Coco", "Soul", "Luca"]
let uppercased = movies.map { title in
title.uppercased()
}
print(uppercased)
["UP", "COCO", "SOUL", "LUCA"]
And filter selecting only short titles:
let shortTitles = movies.filter { $0.count <= 4 }
print(shortTitles)
["Up", "Coco", "Soul", "Luca"]
Note: When a function takes multiple trailing closures, only the first one can use the shorthand trailing syntax. The rest must use their argument labels.
Capturing Values
A closure can capture variables and constants from the surrounding scope where it’s defined. The closure keeps a reference to those values and can read or modify them later — even after the original scope has ended.
Here’s a practical example — a function that creates a score counter:
func makeScoreTracker(for movie: String) -> () -> Int {
var score = 0
let tracker: () -> Int = {
score += 1
return score
}
return tracker
}
let nemoScore = makeScoreTracker(for: "Finding Nemo")
print(nemoScore())
print(nemoScore())
print(nemoScore())
1
2
3
Each time you call nemoScore(), the closure increments the captured score variable. The closure “remembers” the
variable even though makeScoreTracker has already returned. It’s like WALL-E carrying his directive with him long
after he left the ship — the context travels with the closure.
Warning: Because closures capture variables by reference, modifying a captured variable inside one closure affects every other closure that captured the same variable. Be mindful of shared mutable state.
@escaping vs Non-Escaping
By default, closures passed to functions are non-escaping — they run before the function returns and cannot be stored for later.
An @escaping closure is one that outlives the function it was passed to. You’ll see this most often with
completion handlers and asynchronous code:
func fetchMovie(completion: @escaping (String) -> Void) {
// Simulating async work
DispatchQueue.main.async {
completion("Finding Dory")
}
}
fetchMovie { movieName in
print("Fetched: \(movieName)")
}
Fetched: Finding Dory
The closure passed to fetchMovie doesn’t run immediately — it’s stored and called later when the async work finishes.
That’s why it needs the @escaping marker.
Non-escaping closures (the default) are simpler and safer. Swift can optimize them better because it knows they won’t outlive the function:
func applyDiscount(to price: Double,
using formula: (Double) -> Double) -> Double {
return formula(price)
}
let discounted = applyDiscount(to: 19.99) { $0 * 0.8 }
print(discounted)
15.992
Tip: Prefer non-escaping closures when possible. They’re easier to reason about and the compiler can optimize memory usage since it knows the closure won’t outlive its scope.
Common Mistakes
Using $0 When Clarity Suffers
Shorthand arguments save typing, but they can make complex closures unreadable:
// ❌ Hard to understand what $0 and $1 refer to
let result = movies.sorted { $0.count != $1.count ? $0.count < $1.count : $0 < $1 }
// ✅ Named parameters make the intent clear
let result = movies.sorted { first, second in
if first.count != second.count {
return first.count < second.count
}
return first < second
}
Use $0 and $1 for simple one-line closures. Switch to named parameters as soon as the logic involves conditions or
multiple steps.
Forgetting [weak self] with @escaping Closures
When an @escaping closure captures self, it can create a retain cycle — two objects keeping each other alive
forever, leaking memory:
// ❌ Potential retain cycle — self holds the closure,
// the closure holds self
class MoviePlayer {
var title = "Toy Story"
func loadAsync(completion: @escaping () -> Void) {
DispatchQueue.main.async {
completion()
}
}
func play() {
loadAsync {
print("Playing \(self.title)") // Strong capture
}
}
}
// ✅ Use [weak self] to break the retain cycle
class MoviePlayer {
var title = "Toy Story"
func loadAsync(completion: @escaping () -> Void) {
DispatchQueue.main.async {
completion()
}
}
func play() {
loadAsync { [weak self] in
guard let self else { return }
print("Playing \(self.title)")
}
}
}
The [weak self] capture list tells the closure to hold a weak reference to self, so if the object is deallocated,
the closure won’t keep it alive. We’ll dive deeper into this in the
Memory Management post.
What’s Next?
- Closures are self-contained blocks of code you can pass around like values.
- Full syntax:
{ (params) -> Return in body }— but Swift lets you shorten it dramatically. - Trailing closures move the last closure argument outside the parentheses for cleaner code.
- Closures capture variables from their surrounding scope by reference.
@escapingclosures outlive the function they’re passed to; non-escaping closures (the default) do not.
Closures unlock some of Swift’s most powerful patterns. Head to
Higher-Order Functions to see how map, filter, reduce, and
compactMap use closures to transform collections with elegant, expressive code.