Closures are among the most powerful features in Swift, but they are also a primary source of memory leaks. To master them, you must understand not just how to write them, but where they live in memory.

1. The Foundation: Execution Scope and Stack Frames

To understand “escaping,” we must first understand the Stack.

The Execution Scope

An Execution Scope is the context in which a variable exists. When you call a function, the CPU creates a temporary block of memory called a Stack Frame.

The Stack Frame

The “Stack” is a LIFO (Last-In, First-Out) memory structure.

  • Push: When you call func fetchData(), a new Stack Frame is pushed onto the top of the stack. It stores local variables and the closure itself.

  • Pop: When the function finishes and returns, the frame is “popped”. Everything inside it: variables, constants, and non-escaping closures is instantly destroyed.

2. What is @escaping and What is it “Escaping” from?

By default, Swift closures are non-escaping. This means the closure is physically trapped inside the function’s Stack Frame. It must execute before the function finishes.

When you mark a closure as @escaping, you are telling the compiler: “This closure needs to survive after the Stack Frame is destroyed”.

The Move to the Heap

Since the Stack Frame is deleted when the function returns, an escaping closure must be moved to the Heap. The Heap is a larger, more permanent area of memory. Unlike the Stack, which is managed by the CPU’s stack pointer, memory on the Heap is managed by Automatic Reference Counting (ARC).

Definition: An escaping closure “escapes” the lifespan of the function that received it. It moves from the Stack (temporary) to the Heap (persistent).

Why do we need this?

  1. Asynchronous Work: If you make a network request, the function returns immediately. The completion handler must stay alive on the Heap until the server responds.

  2. Storage: If you assign a closure to a property outside the function, it must escape to remain valid.

3. The @autoclosure Attribute

The @autoclosure attribute is a specialized tool that simplifies your code. It automatically wraps an argument passed to a function into a closure.

func logDebug(_ message: @autoclosure () -> String) {
    if isDebugEnabled {
        print(message()) // Evaluated only if needed
    }
}

logDebug("User not found") // No curly braces {} needed

Have you ever used the nil-coalescing operator? let name = optionalName ?? getFromDatabase()

In Swift, the ?? operator uses @autoclosure for the second value.

  • If optionalName has a value, Swift never calls getFromDatabase().

  • It “lazily” skips the work because it doesn’t need the result.

If it weren’t for @autoclosure, your app would hit the database every single time, even if optionalName was already full!

Technical Benefits

  • Lazy Evaluation: The expression isn’t executed when it is passed to the function. It is only executed if and when the function body calls it. This saves processing power if the result isn’t needed.

  • Combining Attributes: You can use @autoclosure @escaping together if that delayed expression also needs to be stored or run asynchronously.

4. Closure Capture Lists and Memory Management

Closures are Reference Types. When a closure is moved to the Heap, it “captures” any external variables it uses so it can access them later.

How Capturing Works

When a closure captures an object (like self), it increases that object’s Reference Count (RC).

  • Default Capture: Without a capture list, the closure creates a Strong Reference.

  • Capture List [ ]: This allows you to define the rules of ownership.

Breaking Retain Cycles

A Retain Cycle (or Strong Reference Cycle) occurs when an object and a closure hold strong references to each other, preventing RC from ever reaching zero.

  1. Object A holds a strong reference to Closure B.

  2. Closure B (on the Heap) captures Object A strongly.

  3. Result: Neither can be deallocated. This is a memory leak.

To solve this, we use the capture list:

  • [weak self]: Converts the reference to an Optional. If the object dies, self becomes nil. This is the safest way to prevent leaks.

  • [unowned self]: Converts the reference to a non-optional. Use this only if the closure and the object have the exact same lifetime. If the object is deallocated and the closure runs, the app will crash.

Summary Comparison

Feature Non-Escaping Escaping
Location Stack Heap
Execution Before function returns After function returns
Memory Risk None High (Retain Cycles)
ARC Overhead Low (Fast) High (Requires tracking)
Capture List Optional Crucial

Conclusion

Understanding closures requires understanding memory. Use non-escaping by default for performance and safety. Use @escaping for async tasks, but always pair it with a capture list ([weak self]) to ensure your app’s memory remains clean.

When you ask “what is @escaping closure escaping from?”, you’re usually talking about an anonymous function being passed into a method. If that method wants to store that block of code to run later (like after a network request), the code has to “escape” the function’s stack and move to the “heap” so it doesn’t disappear when the function finishes.