A closure captures [weak self] and promotes it with guard let. Can a closure created inside it still keep self alive?
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]thenself?.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… →