Escaping vs non-escaping closures?

1.5.1 · L1 · Interview

Reported at: Meta

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 retain self beyond it.
  • “@escaping always 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 withoutActuallyEscaping do? 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… →