Swift
weak self
Swift blocks
memory management
Swift programming

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

Swift closures capture surrounding values, including self, and that can create retain cycles when an object stores a closure that references itself. This is common in async APIs and callback-driven UI code. Correct weak self handling keeps memory safe while still working cleanly with closure arguments.

Why Retain Cycles Happen

A cycle appears when object A strongly owns closure B, and closure B strongly captures object A. Neither object can be released.

swift
1final class Loader {
2    var completion: ((String) -> Void)?
3
4    func start() {
5        completion = { value in
6            self.handle(value) // strong capture
7        }
8    }
9
10    private func handle(_ value: String) {
11        print(value)
12    }
13}

If completion is long-lived, Loader may never deinitialize.

Standard Safe Pattern with Arguments

Use [weak self] in the capture list and rebind strongly inside the closure body.

swift
1final class Loader {
2    var completion: ((String) -> Void)?
3
4    func start() {
5        completion = { [weak self] value in
6            guard let self else { return }
7            self.handle(value)
8        }
9    }
10
11    private func handle(_ value: String) {
12        print("handled", value)
13    }
14}

The callback argument value is still directly available. You are only changing how self is captured.

Multiple Arguments and Result Types

The same pattern applies to richer closures with multiple parameters.

swift
1func fetchUser(completion: @escaping (Result<String, Error>, Int) -> Void) {
2    DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
3        completion(.success("Ava"), 200)
4    }
5}
6
7final class ViewModel {
8    func load() {
9        fetchUser { [weak self] result, statusCode in
10            guard let self else { return }
11            self.apply(result: result, statusCode: statusCode)
12        }
13    }
14
15    private func apply(result: Result<String, Error>, statusCode: Int) {
16        print(statusCode, result)
17    }
18}

This preserves readability and avoids force unwrap risk.

weak Versus unowned

weak gives optional semantics and safely handles cases where object lifetime ends before callback execution. unowned assumes object is always alive and crashes if that assumption is wrong.

swift
child.onEvent = { [unowned self] code in
    self.route(code)
}

Use unowned only when lifetime ordering is guaranteed by design, such as parent owning child and child callback never outliving parent.

UI Thread Handling Is Separate from Capture Handling

weak self solves memory ownership, not threading. UI updates still must run on main thread.

swift
1service.fetchTitle { [weak self] title in
2    guard let self else { return }
3    DispatchQueue.main.async {
4        self.titleLabel.text = title
5    }
6}

Many bugs happen because developers fix capture logic but forget dispatch context.

Concurrency and Task Capture Style

With Swift concurrency, closures appear in Task blocks too. Capture rules still matter.

swift
1Task { [weak self] in
2    guard let self else { return }
3    let data = await self.loadData()
4    await MainActor.run {
5        self.render(data)
6    }
7}

This keeps lifecycle safe and UI updates actor-correct.

Ownership Checklist for Code Reviews

Before approving callback-heavy code, verify:

  • Who owns the closure.
  • Who is captured strongly.
  • Whether callback can outlive owner.
  • How callback is cleared during teardown.

A small checklist catches most retain-cycle regressions early.

Leak Debugging Workflow

If you suspect weak-capture mistakes, start with Xcode Memory Graph and add deinit logging.

swift
deinit {
    print("ViewModel deinit")
}

If deinit never prints after expected teardown, inspect stored closures and captured references first.

Common Pitfalls

  • Using [weak self] and then force-unwrapping self inside the closure.
  • Using unowned in callbacks that can outlive the owner.
  • Forgetting main-thread dispatch for UI state changes.
  • Applying weak capture to non-escaping closures where no cycle risk exists.
  • Not documenting closure ownership in reusable components.

Summary

  • Retain cycles with Swift closures come from strong mutual references.
  • '[weak self] with guard let self is the safest async callback default.'
  • Closure arguments remain easy to handle with weak capture.
  • 'unowned is valid only under strict lifetime guarantees.'
  • Pair capture safety with main-thread and lifecycle correctness.

Course illustration
Course illustration

All Rights Reserved.