- API fetch
- Array
- Async await
- Class
- Closures
- Computed property
- Concurrency
- Constants
- Data types
- Defer statement
- Dictionary
- Enum
- Escaping closure
- Extension
- For loop
- forEach
- Function
- Generics
- Guard statement
- if let statement
- Inheritance
- inout
- Lazy var
- Operator
- Optionals
- Property observers
- Property wrapper
- Protocol
- String formatting
- String interpolation
- Struct
- Switch statement
- Try catch
- Tuple
- Variables
- While loop
SWIFT
Swift Escaping Closures: Syntax, Usage, and Practical Examples
Escaping closures are a key concept in Swift—especially when dealing with asynchronous operations, delayed execution, or storing closures for later use. By default, closures passed to functions are non-escaping, meaning they must be executed within the function’s scope. If a closure needs to outlive the function it was passed to, you must explicitly mark it with @escaping
.
Understanding when and how to use escaping closures helps ensure correct memory management, prevent retain cycles, and write robust asynchronous code.
What Is an Escaping Closure?
An escaping closure is one that’s called after the function it was passed into has returned. It’s often used in networking, animations, timers, and other delayed or background tasks.
To define one, use the @escaping
attribute in the function parameter:
func performLater(_ closure: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
closure()
}
}
Here, the closure is stored and executed after the function returns—hence the need for @escaping
.
Syntax and Usage
You use @escaping
in the function definition:
func storeClosure(_ completion: @escaping () -> Void) {
closures.append(completion)
}
Use it whenever the closure:
- Is called asynchronously
- Is stored in a property or variable
- Is passed to another escaping function
- Needs to persist beyond the function’s lifetime
This tells Swift to retain the closure beyond the current scope, ensuring safe and predictable behavior.
Escaping vs Non-Escaping Closures
Non-Escaping (Default)
Executed immediately within the function body:
func executeNow(_ closure: () -> Void) {
closure()
}
No need for @escaping
here because the closure doesn’t outlive the function.
Escaping
Executed later or stored for future use:
func executeLater(_ closure: @escaping () -> Void) {
storedClosures.append(closure)
}
The difference lies in timing and storage—escaping closures require the compiler to retain and manage them beyond the function’s execution.
Practical Examples
1. Networking
func fetchData(completion: @escaping (Data?) -> Void) {
URLSession.shared.dataTask(with: someURL) { data, _, _ in
completion(data)
}.resume()
}
The completion handler runs after the data task completes, so it must be escaping.
2. Storing for Later
var storedClosures: [() -> Void] = []
func remember(_ closure: @escaping () -> Void) {
storedClosures.append(closure)
}
Since the closures may be called much later, they must be marked with @escaping
.
Optional Escaping Closures
You can also use optional escaping closures:
func performAction(callback: (@escaping () -> Void)?) {
if let callback = callback {
storedClosures.append(callback)
}
}
This is useful in APIs where the caller may or may not provide a closure.
Capturing self
in Escaping Closures
One of the biggest concerns with escaping closures is retain cycles. When you reference self
inside an escaping closure, you must capture it explicitly.
Risk of a Retain Cycle:
class ViewModel {
func fetch() {
networkService.request { self.handleResponse() }
}
}
Safe Approach:
networkService.request { [weak self] in
self?.handleResponse()
}
Use [weak self]
or [unowned self]
depending on whether self
can be deallocated. This prevents memory leaks.
Real-World Use Cases
Escaping closures are essential in many common scenarios:
- Asynchronous APIs: Completion handlers for network requests, animations, or background tasks
- Event handling: For user interactions, system notifications, or reactive bindings
- Dependency injection: Passing closures that configure services or return dependencies
- Retry logic: Storing closures to re-execute failed operations
These patterns all involve closures that must remain alive beyond the scope of the function where they’re defined.
Best Practices
To use escaping closures effectively:
- Only mark closures as
@escaping
when necessary—avoid overuse - Always capture
self
weakly in classes to avoid retain cycles - Label closure parameters clearly (e.g.,
completion
,handler
,callback
) - Use
Result<T, Error>
in your closure signatures for robust error handling - Keep closure logic concise and avoid deeply nested escaping calls
Considerations and Limitations
- You can’t use
inout
parameters with escaping closures. - They may execute on different threads, so thread safety matters.
- Memory leaks can occur if retain cycles aren’t broken.
- They add complexity to testing and debugging due to delayed execution.
Understanding these trade-offs helps you use escaping closures appropriately.
Summary
Escaping closures allow deferred execution beyond the lifetime of the function they’re passed to. They’re indispensable in asynchronous programming, but they come with responsibilities—particularly around memory and lifecycle management.
By marking closures with @escaping
only when needed, managing references to self
, and structuring your code clearly, you can build safe and efficient APIs that handle asynchronous logic gracefully.
Sign up or download Mimo from the App Store or Google Play to enhance your programming skills and prepare for a career in tech.