Ⅰ.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
2
3
4
5
func fetchData() -> String? {
URLSession.shared.dataTask(with: url) { data, response, error in
return "Data received!" // ❌ ERROR
}.resume()
}

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
2
3
4
5
6
7
8
9
10
11
12
func fetchData(completion: @escaping (String?) -> Void) {
let url = URL(string: "https://example.com")!
URLSession.shared.dataTask(with: url) { data, _, _ in
if let data = data {
let str = String(data: data, encoding: .utf8)
completion(str)
// ✅ returning data asynchronously
} else {
completion(nil)
}
}.resume()
}

Usage (with trailing closure syntax):

1
2
3
4
5
6
7
fetchData { result in
if let result = result {
print("Got response: \(result)")
} else {
print("Failed to get response.")
}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class DataManager {
func processData(closure: (String) -> Void) {
print("Start processing data")

let result = "Local data processed"

// Execute the closure immediately (non-escaping)
closure(result)

print("End processing data")
}
}

class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()

let manager = DataManager()

manager.processData { result in
print("Closure executed with result: \(result)")
}
}
}

Console Output:

1
2
3
Start processing data  
Closure executed with result: Local data processed
End 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
getData { (data) in
print("Closure returned -- \(data) -- \(Thread.current)")
}
}

func getData(closure: @escaping (Any) -> Void) {
print("Function started -- \(Thread.current)")
DispatchQueue.global().async {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print("Executing closure --- \(Thread.current)")
closure("345")
}
}
print("Function ended --- \(Thread.current)")
}
}

Execution Output:

1
2
3
4
Function started -- <NSThread: ...>{number = 1, name = main}
Function ended --- <NSThread: ...>{number = 1, name = main}
Executing closure --- <NSThread: ...>{number = 1, name = main}
Closure returned -- 345 -- <NSThread: ...>{number = 1, name = main}

Code Example: Non-Escaping Closure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
handleData { (data) in
print("Closure returned -- \(data) -- \(Thread.current)")
}
}

func handleData(closure: (Any) -> Void) {
print("Function started -- \(Thread.current)")
print("Executing closure --- \(Thread.current)")
closure("4456")
print("Function ended --- \(Thread.current)")
}
}

Execution Output:

1
2
3
4
Function started -- <NSThread: ...>{number = 1, name = main}
Executing closure --- <NSThread: ...>{number = 1, name = main}
Closure returned -- 4456 -- <NSThread: ...>{number = 1, name = main}
Function ended --- <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
2
3
4
5
6
7
class DataLoader {
var completionHandler: (() -> Void)?

func loadData(completion: @escaping () -> Void) {
self.completionHandler = completion // 💥 Potential retain cycle if [weak self] is not used
}
}

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
2
3
4
5
6
func loadData(completion: @escaping () -> Void) {
self.completionHandler = { [weak self] in
// Use self? safely
self?.doSomething()
}
}

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


  1. Apple Developer Documentation. Closures – The Swift Programming Language (Swift 5.9)
  2. Apple Developer. SwiftUI Essentials – Handling User Input with Closures
  3. Swift.org. Swift Evolution Proposal SE-0279: Multiple Trailing Closures
  4. Hacking with Swift. What is a trailing closure in Swift?
  5. Swift中的逃逸闭包(@escaping )与非逃逸闭包(@noescaping)Swift中的逃逸闭包(@escaping )与非逃逸闭包(@noescaping)