Swift
iOS Development
Memory Management
Weak Self
Swift Blocks

How to Correctly handle Weak Self in Swift Blocks with Arguments

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Closures that capture self can easily create retain cycles when the owning object also retains the closure or the object that owns it. The usual Swift fix is to capture self weakly and unwrap it when the closure runs. The fact that the closure also has arguments does not change that pattern. It only changes where you place the unwrapping logic.

Why weak self Exists

Suppose a view controller starts an asynchronous request and the completion closure references the controller. If the request object retains the closure and the controller retains the request, a strong cycle can keep both alive longer than intended.

The standard Swift pattern is a capture list:

swift
1service.loadUser { [weak self] user in
2    guard let self = self else { return }
3    self.render(user)
4}

The closure still receives user normally. Only self is captured weakly.

Unwrap self Early in Multi-Step Closures

If the closure does more than one thing, unwrap self at the top and then use it like a normal strong reference for the rest of the block.

swift
1class ProfileViewController: UIViewController {
2    let service = UserService()
3
4    func refresh() {
5        service.loadUser { [weak self] user in
6            guard let self = self else { return }
7            self.title = user.name
8            self.updateAvatar(with: user.avatarURL)
9            self.showLastUpdated()
10        }
11    }
12
13    private func updateAvatar(with url: URL) {
14        print("Load avatar from \(url)")
15    }
16
17    private func showLastUpdated() {
18        print("UI updated")
19    }
20}

This is usually clearer than scattering self? through many lines. It also documents intent: if the owner is gone, the rest of the closure should not run.

Optional Chaining Is Fine for Small Closures

For very small closures, optional chaining is often enough:

swift
timer.start { [weak self] seconds in
    self?.countdownLabel.text = "\(seconds)s"
}

That is concise and readable when the closure only performs one side effect. Once the closure grows, guard let self = self else { return } usually becomes easier to maintain.

Closures with Multiple Arguments Work the Same Way

Arguments do not change the weak-capture pattern at all. You still capture self in the capture list and unwrap it inside the closure.

swift
1api.fetchImage { [weak self] image, error in
2    guard let self = self else { return }
3
4    if let error = error {
5        self.presentError(error)
6        return
7    }
8
9    if let image = image {
10        self.imageView.image = image
11    }
12}

The closure parameters remain regular inputs. weak self only changes how the surrounding object is retained.

weak Versus unowned

weak self makes self optional inside the closure because the object may disappear before the closure runs. unowned self assumes the object will definitely still exist.

swift
transitionCoordinator?.animate(alongsideTransition: { [unowned self] _ in
    self.view.alpha = 1.0
})

This can be correct for short-lived framework callbacks where the lifetime relationship is guaranteed. It is usually risky for network callbacks, timers, or delayed work. If the guarantee is wrong, unowned can crash.

When You Do Not Need weak self

Not every closure needs a weak capture. A non-escaping closure or a closure that is not retained beyond the current call often does not create a cycle in the first place.

Overusing [weak self] everywhere can make code noisier without fixing an actual memory problem. The right question is whether the closure can outlive the current scope while also holding on to the object.

Common Pitfalls

The biggest mistake is using [weak self] and then force-unwrapping self! later. That defeats the whole safety benefit and can still crash.

Another issue is using [unowned self] for genuinely asynchronous work such as network completions or timers. Those are exactly the cases where self may be gone before the closure runs.

People also often do a lot of unrelated work before checking whether self still exists. If the closure's purpose is to update self, unwrap early and return immediately if it has already been deallocated.

Finally, do not cargo-cult weak capture into every closure. Use it when the lifetime graph actually makes a retain cycle possible.

Summary

  • Use [weak self] when a closure may outlive the object and create a retain cycle.
  • Unwrap self early for multi-step closures with guard let self = self else { return }.
  • Use self? for short one-line closures when readability stays good.
  • Reserve [unowned self] for cases with a real lifetime guarantee.
  • Closure arguments do not change the weak-capture pattern; they simply coexist with it.

Course illustration
Course illustration

All Rights Reserved.