Escaping vs non-escaping closures?
Predict first
Does this compile?
import Dispatch
func later(_ work: () -> Void) {
DispatchQueue.main.async(execute: work)
}
- Yes
- No
Mental model. Non-escaping is a phone call that ends before you leave the room. Escaping is a voicemail that outlives the conversation.
Non-escaping (the default for parameters) is guaranteed to be called before the function returns. The compiler can skip retains, keep captures on the stack, and it cannot create a retain cycle — nothing stores it.
Escaping (@escaping) may be stored and called later: DispatchQueue.async, completion handlers, Combine sinks. Because it outlives the call, capturing self strongly can extend the object's lifetime and form a cycle.
The queue stores work and calls it after later returns, so the parameter must be marked @escaping.
Common traps
- “Non-escaping closures need
[weak self].” They never do — a non-escaping closure cannot outlive the call, so it cannot retainselfbeyond it. - “
@escapingalways means a leak.” It means “may outlive”. A leak needs a cycle: the object also strongly retains the closure.
They might follow up with
- Why can non-escaping captures stay on the stack? The compiler knows the closure's lifetime is bounded by the call, so no heap context is required.
- What does
withoutActuallyEscapingdo? It lets you pass a non-escaping closure to an API that claims it escapes, while asserting it really does not.
Related questions
- 1.5.4 What does a capture list like [weak self], [unowned self], [self], or [foo] actually do?
- 1.5.2 guard let self = self else { return } inside a closure — what does it do to ARC?
guard let self = self else { return } inside a closure — what does it… →