Closures
Functions you can pass around — and the variables they quietly take with them.
A closure is a block of code you can store in a variable, pass to a function, and call later. That much is just "a function without a name".
The important half is in the word *closure*: it closes over the variables around it. When you write a closure that mentions count, it doesn't copy the current value of count and forget where it came from — it keeps a reference to the actual storage, and keeps that storage alive for as long as the closure exists.
That's what makes closures powerful, and it's also the source of nearly every retain cycle in an iOS codebase.
count outlives makeCounter because the returned closure captured it.// A closure stored in a variablelet double: (Int) -> Int = { n in n * 2 }
// Captured storage is shared, not copiedfunc makeCounter() -> () -> Int { var count = 0 // lives on past this function returning return { count += 1; return count }}let next = makeCounter()next() // 1next() // 2 — the same `count`, still alive
// Trailing closure syntaxlet evens = (1...10).filter { $0.isMultiple(of: 2) }Escaping and non-escaping
Escaping vs non-escaping
A closure parameter is *non-escaping* by default: the function promises to call it before returning and not store it anywhere. That promise is what lets you use self inside without writing self., because there's no way the closure can outlive the call.
Mark it @escaping and the promise is gone — the closure may be stored, queued, or called later. Now Swift makes you write self explicitly, precisely so that capturing self is a visible decision rather than an accident.
That required self. isn't bureaucracy. It's the compiler asking: *are you sure you want this object kept alive until this closure runs?*
self. in escaping closures so the capture is deliberate.final class Loader { var onFinish: (() -> Void)?
// non-escaping: called before we return, no `self.` needed func each(_ items: [Int], _ body: (Int) -> Void) { for item in items { body(item) } }
// @escaping: stored for later — `self.` is now mandatory func whenDone(_ handler: @escaping () -> Void) { onFinish = handler }
func start() { whenDone { [weak self] in self?.cleanUp() // weak: the closure no longer keeps Loader alive } }
func cleanUp() {}}
Show all 21 linesShow less
final class Loader { var onFinish: (() -> Void)?
// non-escaping: called before we return, no `self.` needed func each(_ items: [Int], _ body: (Int) -> Void) { for item in items { body(item) } }
// @escaping: stored for later — `self.` is now mandatory func whenDone(_ handler: @escaping () -> Void) { onFinish = handler }
func start() { whenDone { [weak self] in self?.cleanUp() // weak: the closure no longer keeps Loader alive } }
func cleanUp() {}}Under the hood
A closure is two things bolted together: a function pointer, and a heap-allocated *context box* holding the captured variables. Swift calls this pair a thick function value.
That box is reference counted like any object. Captured vars are promoted from the stack to that box — which is why count in the example survives makeCounter returning, and why two closures capturing the same variable genuinely share it rather than each getting a copy.
A non-escaping closure can skip the heap entirely: since it provably doesn't outlive the call, the compiler can keep its context on the stack. That's the real reason non-escaping is the default — it's the faster case, and @escaping is you opting into the allocation.
Quick check
What does this print?
- 0 0
- 1 2
- 1 1
- 2 2
Reveal answer
1 2
Both calls go through the same closure, which captured the *variable* count, not its value at the time of capture. The captured storage is promoted to a heap box that outlives makeCounter, so the increments accumulate. If closures copied values at capture time you'd see 1 1.
Interview question
A colleague adds [weak self] to every closure in the codebase "to be safe". What's your response?
Think first, then reveal
It's cargo-culting a real rule, and it introduces a different class of bug.
[weak self] is the right tool when the closure is stored somewhere self can reach — a stored property, a subscription, a delegate-ish callback — because that's the shape that creates a cycle. It does nothing useful for a non-escaping closure like the one you pass to map or forEach: that closure cannot outlive the call, so there's no cycle to break, and you've added an optional unwrap and noise for nothing.
Worse, it's actively wrong for one-shot escaping work you *want* to complete. If a network completion handler weakly captures self and the object is deallocated mid-flight, the handler runs and silently does nothing — no save, no cleanup, no log. That's a bug that never crashes and never appears in a stack trace, so it's much harder to find than the leak it was meant to prevent.
So: reach for [weak self] when the closure's lifetime is owned by self, and think about whether the work needs to finish before you weaken the reference that keeps it alive.
What an interviewer listens for
- Knows non-escaping closures can't create cycles, so weak there is pure noise
- Identifies that weak self can silently skip work that needed to happen
- Frames the decision around who owns the closure's lifetime
- Doesn't reduce it to a blanket rule in either direction
Key takeaway
A closure captures storage, not values, and keeps that storage alive. Ask of every escaping closure: what does this keep alive, and for how long?