Swift & Objective-C

[Swift Basics #2] Closure Captures, weak self, and escaping

Closures are everywhere in Swift code. But many developers still struggle to explain exactly what capture means, why [weak self] is used, and why @escaping is needed.

6 min read
Cover image for [Swift Basics #2] Closure Captures, weak self, and escaping

Closures are everywhere in Swift code: sorting conditions, network completion handlers, button actions, and even SwiftUI’s body. Code that passes a brace-delimited block appears dozens of times a day. Yet many developers struggle to explain exactly what it means for a closure to capture a value, why [weak self] is used, and why @escaping is needed.

These three questions are really one. Once you understand how a closure captures surrounding values, the reasons closures are reference types, retain cycles occur, and escaping must be marked all follow naturally. This second Swift basics article traces those connections in order.

What Is a Closure? — The Important Property Is “Capturing”

Let’s briefly cover the syntax. A closure is executable code treated as a value. You can store it in a variable, pass it as an argument, or return it.

let add: (Int, Int) -> Int = { a, b in a + b }
add(2, 3) // 5

In fact, a function declared with func is a named closure. In Swift, functions and closures belong to the same family; { } is simply the unnamed, inline form.

The name closure comes not from being a “code block,” but from its ability to close over surrounding variables.

func makeCounter() -> () -> Int {
    var count = 0
    return {
        count += 1
        return count
    }
}

let counter = makeCounter()
counter() // 1
counter() // 2
counter() // 3

Something unusual is happening. count is a local variable in makeCounter, so it should disappear when the function returns. Yet it keeps increasing whenever the returned closure is called. The closure captures the variable count outside its own body and keeps it alive after the function ends. That is the essence of closures, and everything else in this article follows from capture.

What Capture Really Means — A Reference, Not a Copy

Swift closures capture by reference by default. They do not take a copy; they remain connected to the variable itself. That produces this result.

var multiplier = 2
let times = { (n: Int) in n * multiplier }

times(10)      // 20
multiplier = 3
times(10)      // 30 — The closure sees the changed value

The multiplier used is 3 at execution time, not 2 when the closure was created. A closure carries the variable itself, not a snapshot of it.

Use a capture list when you want to freeze the value from creation time.

let times = { [multiplier] (n: Int) in n * multiplier }

times(10)      // 20
multiplier = 3
times(10)      // 20 — Fixed 2at creation time

Variables written inside the brackets are copied when the closure is created and embedded as constants. In short: the default is reference capture—a live connection—while a capture list is value capture—a creation-time snapshot. This distinction must come first before explaining [weak self].

Closures are reference types because of this capture storage. Captured variables must share the closure’s lifetime, so they are stored on the heap; copying a closure value adds another reference to the same storage. That is why closures behave like classes in a struct-centered language such as Swift. If value types and reference types are unfamiliar, read the value-types-first article first.

Default capture is a live connection; a capture list is a creation-time snapshot
Default capture is a live connection; a capture list is a creation-time snapshot

Retain Cycles and [weak self] — The Classic Problem Caused by Capture

The price of reference capture is a retain cycle. The structure is always the same.

class ProfileViewModel {
    var onUpdate: (() -> Void)?
    var name = ""

    func bind() {
        onUpdate = {
            print("name: \(self.name)")
        }
    }
}

The view model strongly owns the closure through its onUpdate property. That closure, in turn, captures self—the view model—by reference. Ownership closes the loop: view model → closure → view model. In the ARC world, neither is released because they retain each other. Even after the screen closes, the view model remains in memory: a leak.

The solution is the second use of a capture list. [weak self] instructs the closure to capture self weakly. The closure retains self without owning it, breaking the cycle. Since self may be released first, self becomes optional inside the closure, which commonly begins with guard let self else { return }. The early-exit pattern from the optionals article appears here again.

onUpdate = { [weak self] in
    guard let self else { return }
    print("name: \(self.name)")
}

The important point is that [weak self] is not a universal prefix. A cycle requires self to own the closure while the closure captures self. Without that condition, weak is unnecessary. For example, the closure passed to DispatchQueue.main.asyncAfter is discarded by the system after execution; self merely lives slightly longer, which is not a leak. Instead of adding weak reflexively, ask who owns this closure and for how long. Cases with unusual ownership, such as NSTimer, are covered separately.

@escaping — When a Closure Outlives Its Function

Here is the final puzzle piece. Closures received as function parameters sometimes have @escaping attached.

func fetchUser(completion: @escaping (User) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, _ in
        let user = parse(data)
        completion(user)   // Executed long after the function returns
    }.resume()
}

The distinction is based on execution time. A closure executed and discarded before the function returns is non-escaping, the default. If it can be stored in a property or passed to asynchronous work and run after the function returns, it is escaping—a closure that leaves the function.

Why enforce the distinction? Because the compiler and developer reason differently depending on whether the closure escapes. A non-escaping closure is guaranteed to live only during the function call, allowing the compiler to optimize capture storage and eliminating retain-cycle concerns at the source. That is why you need not worry about self in closures passed to map or filter. An escaping closure, by contrast, may be stored somewhere and live longer, so it becomes a candidate for the retain-cycle review described above. @escaping is an API-level warning label: this closure may live longer, so pay attention to capture.

Completion-handler-style escaping closures are becoming less common in new code with async/await, but you still need to understand them precisely when reading and bridging existing APIs.

weak breaks the cycle in which the two objects retain each other
weak breaks the cycle in which the two objects retain each other

Three Practical Rules

Reduced to practical rules, there are three.

First, ask about lifetime when you see a closure. Is it consumed inside the function and then discarded (non-escaping), or stored somewhere to live longer (escaping)? This single question determines how carefully you need to consider capture.

Second, use weak by judgment, not reflex. Where ownership really closes a cycle—a handler stored in a property or a delegate-like callback—use [weak self]. It is unnecessary for one-shot execution without a cycle. When uncertain, verify with Instruments’ Leaks tool or deinit logs.

Third, document intent with a capture list. To freeze a value, use [value]; to avoid ownership, use [weak self]. A capture list is documentation in code of how the closure connects to the outside world, before it is a performance mechanism.

Summary

  • The essence of a closure is not a code block but capture: closing over surrounding variables and extending their lifetime.
  • Default capture is a reference, not a copy, so the closure sees the value at execution time; the capture list [x] freezes the value from creation time.
  • Closures are reference types because their capture storage must be shared.
  • When a closure owned by self captures self, a retain cycle occurs. [weak self] plus guard let self is the standard solution. If there is no ownership cycle, weak is unnecessary.
  • @escaping is a warning label meaning “a closure that outlives the function,” marking places where capture must be reviewed.

In the next article, we’ll organize the choices hidden in a single variable declaration: stored and computed properties, lazy, and property observers.