A closure captures [weak self] and promotes it with guard let. Can a closure created inside it still keep self alive?

3.9.2 · L3 · Interview

Predict first

When does each view controller die?

final class VC {
    let name: String
    var pending: [() -> Void] = []
    init(_ n: String) { name = n }
    deinit { print("deinit \(name)") }
    func strongInner() {
        pending.append { [weak self] in
            guard let self else { return }
            Later.queue.append { print("inner ran:", self.name) }
        }
    }
    func weakInner() {
        pending.append { [weak self] in
            Later.queue.append { [weak self] in print("inner ran, self is", self == nil ? "nil" : "alive") }
        }
    }
}
enum Later { nonisolated(unsafe) static var queue: [() -> Void] = [] }
do {
    let vc = VC("A")
    vc.strongInner()
    vc.pending[0]()
}
print("A out of scope, queue holds \(Later.queue.count) closure")
Later.queue.removeAll()
print("queue cleared")
do {
    let vc = VC("B")
    vc.weakInner()
    vc.pending[0]()
}
print("B out of scope")
Later.queue.forEach { $0() }
  • A: “A out of scope, queue holds 1 closure”, queue cleared, deinit A; B: deinit B, B out of scope, inner ran, self is nil
  • Both deinit before their “out of scope” line
  • Neither is ever deallocated

Mental model. Lending your car to a friend with “bring it back when you are done” (weak) is fine; but if the friend lends it onward without that condition, you do not get it back until the second borrower is finished.

Yes. [weak self] protects only the closure it is written on. Inside it, guard let self creates a strong local self, and any closure you create inside that scope and store captures that strong reference. In the check strongInner parks an inner closure in Later.queue: after the view controller’s variable goes out of scope it is not deallocated, because the queued closure owns it (the queue holds 1 closure, and deinit A only prints after the queue is cleared). In weakInner the inner closure writes [weak self] again, so deinit B prints at once and the closure later finds self nil.

The rule is to repeat the capture list at every escaping level and do not promote self to strong across a delay: promote at the moment of use (guard let self as the first line of the closure that actually runs). Non-escaping closures (map, forEach) are safe to use strong self inside. A strong promotion that lasts for a short synchronous stretch is fine and indeed useful (it makes the stretch atomic with respect to deallocation, see Q505); the bug is promotion followed by storing or scheduling more work that outlives the call. A memory-graph screenshot shows it as the object reachable from a queue or timer rather than from a cycle, so it looks like a delayed release, not a leak.

The queued closure promoted self to a strong reference in A; B repeated [weak self] on the inner closure.

Common traps

  • Writing [weak self] on the outer closure only. The inner escaping closure uses the promoted strong self.
  • Treating a delayed deinit as proof of a cycle. It may just be a stored task that still holds self; it ends when the work does.

They might follow up with

  • Does the same happen with Task {}? Yes, a task body that captures a promoted self holds it for the task’s duration (Q143).
  • Is [weak self] then self?. on each line better? It keeps self from being kept alive but allows half-done work; Q505 compares the two.

Related questions

  • 3.3.3 When does a closure create a cycle, and why doesn’t a lazy var closure always?
  • 3.3.6 Can a Task cause a retain cycle, and how do you avoid it?
  • 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?
  • 27.1.3 A closure uses [weak self] and the owner is released while the work is running. What is the difference…

← How do you hold a collection of objects without owning them: WeakBox…   What does copying a struct that holds class references cost, and how… →