Always pass weak reference of self into block in ARC?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
The advice to "always use weak self in blocks" is an oversimplification. You should use [weak self] or [unowned self] only when the block is stored by self (or an object owned by self), creating a retain cycle. If the block is short-lived (e.g., passed to UIView.animate or DispatchQueue.main.async), capturing self strongly is safe and often preferred because it guarantees self is alive during execution. The rule is: use weak self when there is a cycle, not unconditionally.
When You Need weak self (Retain Cycle)
The cycle: self owns onComplete (stored property), and the block captures self. Neither can be deallocated.
When You Do NOT Need weak self
Short-lived blocks (no cycle)
These blocks are owned by the system (UIKit, GCD, stdlib), not by self. Once the block executes, it is released, and its strong reference to self goes away. No cycle exists.
Intentionally keeping self alive
Using [weak self] here would cause self to be deallocated if the user navigates away, and the response would be silently dropped. Sometimes strong capture is the desired behavior.
weak self vs unowned self
| Capture | Behavior when deallocated | Use when |
[weak self] | self becomes nil | Self might be deallocated before block runs |
[unowned self] | Crash (dangling pointer) | You guarantee self outlives the block |
| Strong (default) | Keeps self alive | Block is short-lived with no cycle |
The guard let self = self Pattern
guard let self = self (Swift 4.2+) creates a strong local reference for the rest of the closure, preventing self from being deallocated mid-execution.
Objective-C Equivalent
The two-step pattern (__weak outside, __strong inside) prevents retain cycles while ensuring self is not deallocated during block execution.
Common Retain Cycle Patterns
Decision Flowchart
Common Pitfalls
- Using
[weak self]everywhere blindly: Unnecessaryweak selfadds optional unwrapping boilerplate and can causeselfto be deallocated before the block completes. Only use it when there is an actual retain cycle or when you explicitly want to allow early deallocation. - Using
[unowned self]when self might be deallocated:unownedis a performance optimization overweak(no optional unwrapping) but crashes ifselfis deallocated. Only use it when you can guarantee the object outlives the closure (e.g., a closure that is always invalidated indeinit). - Forgetting to invalidate timers:
Timer.scheduledTimerretains its target. Even with[weak self]in the block variant, the non-blockscheduledTimer(target:)retainstargetstrongly. Always invalidate timers indeinitorviewWillDisappear. - Not recognizing escaping vs non-escaping closures: Non-escaping closures (like
Array.map,Array.filter) execute synchronously and are released immediately — no cycle possible. Escaping closures (@escaping) can be stored and may cause cycles. The compiler marks closures as@escapingwhen they outlive the function call. - Retain cycles through intermediate objects:
self → manager → completion block → selfis a cycle even thoughselfdoes not directly own the block. Trace the full ownership chain to identify cycles.
Summary
- Use
[weak self]when the block is stored byselfor an object owned byself(retain cycle) - Do NOT use
[weak self]for short-lived blocks (animations, GCD dispatch, map/filter) — no cycle exists - Use
guard let self = self else { return }inside weak-self closures to prevent mid-execution deallocation - Prefer
[weak self]over[unowned self]unless you can guarantee the object's lifetime - The decision depends on ownership: who stores the block, and does the block reference the owner?

