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.

Learn to Code in Swift for Free
Start learning now
button icon
To advance beyond this tutorial and learn Swift by doing, try the interactive experience of Mimo. Whether you're starting from scratch or brushing up your coding skills, Mimo helps you take your coding journey above and beyond.

Sign up or download Mimo from the App Store or Google Play to enhance your programming skills and prepare for a career in tech.

You can code, too.

© 2025 Mimo GmbH

Reach your coding goals faster