Escaping vs Non-Escaping Closures in Swift
Ⅰ.What is an Escaping Closure?
Definition:
An escaping closure is a closure that is passed into a function as a parameter, but may be called after the function has already returned.
In other words, the closure “escapes” the function’s scope.
When declaring a function that takes a closure parameter, you use the @escaping keyword to indicate that the closure is allowed to escape the function body.
This means the closure “escapes” the function’s body.
Ⅱ. Why Do We Need Escaping Closures? (Example: Networking)
Imagine a function like this:
1 | func fetchData() -> String? { |
A closure that is called when a network request completes is escaping — the request is sent, time passes, and the closure is only executed when the response comes back, outside the original function scope.
This won’t work because:
- return here is trying to return from the closure, not the outer fetchData().
- The function finishes before the network call completes.
When you call fetchData:
- You immediately execute .resume(), and the request is sent out right away ✅
- But the completion closure doesn’t run immediately — it will only execute after the server responds, which could take a few hundred milliseconds or even several seconds ❗️
The request is indeed sent out immediately, but the closure you pass in is meant to handle what happens after the result comes back — and that result doesn’t arrive right away.
So we need a way to handle results later — that’s what escaping closures are for.
Ⅲ. What Does @escaping Do?
By default, Swift closures passed as parameters are non-escaping, meaning:
- The closure must be executed within the function’s body
- It can be stored on the stack, which is safer and more performant
But for asynchronous operations, like network requests:
- The closure must outlive the function → that’s when you use @escaping
- Swift stores it on the heap to execute later
The network request is sent immediately, but the closure that handles the response is executed at a later time — that’s why it must be marked as an escaping closure (@escaping).
Ⅳ.Correct Way Using an Escaping Closure
1 | func fetchData(completion: @escaping (String?) -> Void) { |
Usage (with trailing closure syntax):
1 | fetchData { result in |
Why is @escaping Required?
Because the completion closure is called after fetchData() has already returned, it “escapes” the function’s local scope.
Swift requires @escaping in such cases to make the closure’s lifetime explicit.
Ⅴ.Non-Escaping Closures
Concept:
A non-escaping closure is a closure passed into a function and executed within the function before it returns.
Example: Using a Non-Escaping Closure to Process Local Data
1 | class DataManager { |
Console Output:
1 | Start processing data |
Explanation:
- The method processData takes a closure of type (String) -> Void.
- The closure is executed immediately inside the function body.
- Since the closure does not escape the function scope, it does not require the @escaping keyword.
- The compiler can safely assume that the closure’s lifetime ends within the function, so it’s fully safe and optimized.
Ⅵ.Escaping Closure VS Non-Escaping Closures
Let review another example :
Code Example: Escaping Closure:
1 | class ViewController: UIViewController { |
Execution Output:
1 | Function started -- <NSThread: ...>{number = 1, name = main} |
Code Example: Non-Escaping Closure:
1 | class ViewController: UIViewController { |
Execution Output:
1 | Function started -- <NSThread: ...>{number = 1, name = main} |
Feature | Example 1 (Escaping) | Example 2 (Non-Escaping) |
---|---|---|
Closure keyword. | @escaping | No @escaping |
Execution timing | Delayed, after the function returns | immediately within the function |
Async or delayed? | ✅ Yes | ❌ No |
Escapes function scope? | ✅ Yes | ❌ No |
Compiler requires escaping | ✅ Yes | ❌ No |
Output sequence | Start → End → Closure | Start → Closure → End. |
Ⅶ.Limitations of Escaping Closures
While escaping closures are powerful and necessary for handling asynchronous operations, they come with certain risks — particularly around memory management.
Risk of Retain Cycles
One of the most important caveats when using @escaping closures is the potential for retain cycles. Since escaping closures are stored and executed later, they can strongly capture self, preventing it from being deallocated.
Example:
1 | class DataLoader { |
In the above example, self retains completionHandler, and if the closure also captures self, a retain cycle occurs — meaning both self and the closure will never be released from memory.
Best Practice:
Be careful: Escaping closures can capture self strongly, which may lead to retain cycles.
To avoid memory leaks, use [weak self] or [unowned self] when referencing self inside escaping closures.
Safer Example:
1 | func loadData(completion: @escaping () -> Void) { |
This approach prevents memory from being held unnecessarily, and ensures a safe, leak-free implementation.
Ⅸ.Summary
We can think of a running program as a car cruising on a highway.
When everything is synchronous—no network requests, no delayed tasks—it’s like driving straight down a clear, straight road. You don’t have to steer much or worry about detours; everything proceeds predictably and smoothly.
However, the moment you introduce asynchronous operations—like network calls, timers, or background processing—things change. It’s as if the road starts to curve. Now, you have to turn the wheel, adjust your path, and sometimes even wait at intersections for responses before moving forward.
In this scenario, the closures you pass into functions can no longer be executed immediately. They “escape” the straight road of the function and are triggered later, when the response arrives. These closures must be marked with @escaping to let the Swift compiler know:
“This closure won’t finish instantly—it will leave the function and come back later. Keep it safe until then.”
So at its core, the purpose of @escaping is:
To allow delayed tasks the space and permission to live beyond the function’s lifetime, so they can be safely executed when the time is right.
Ⅹ.References
- Apple Developer Documentation. Closures – The Swift Programming Language (Swift 5.9)
- Apple Developer. SwiftUI Essentials – Handling User Input with Closures
- Swift.org. Swift Evolution Proposal SE-0279: Multiple Trailing Closures
- Hacking with Swift. What is a trailing closure in Swift?
- Swift中的逃逸闭包(@escaping )与非逃逸闭包(@noescaping)Swift中的逃逸闭包(@escaping )与非逃逸闭包(@noescaping)