iosNudge

What is an escaping closure in Swift?

EasyAsked oftenSwiftMemory & ARC 2 min read

TL;DR

A closure parameter is non-escaping by default. It must run before the function returns. If the function stores the closure or calls it later, the closure escapes. You must mark it @escaping. Escaping closures can outlive the function, so they can create retain cycles. Swift asks you to write self explicitly inside them.

Non-escaping (the default)

func repeatTwice(_ action: () -> Void) {
    action()
    action()
}   // the closure is done here

repeatTwice { print("Hi") }

map, filter and forEach take non-escaping closures. They run them right away.

Escaping: stored for later

final class Button {
    private var onTap: (() -> Void)?

    func setAction(_ action: @escaping () -> Void) {
        onTap = action          // stored, so it escapes
    }
}

Without @escaping you get a compile error: "Assigning non-escaping parameter 'action' to an @escaping closure".

Escaping: called asynchronously

func loadUser(completion: @escaping (User) -> Void) {
    DispatchQueue.global().async {
        let user = User(name: "Ana")
        completion(user)        // runs after loadUser returned
    }
}

Classic completion handlers are always @escaping.

Why Swift cares

Non-escapingEscaping
Runs after function returnsNoYes
Can be storedNoYes
Implicit self in classesAllowedYou must write self. or capture it
Retain cycle riskNoYes
OptimizationContext can stay on the stackContext goes to the heap

Because a non-escaping closure cannot outlive the call, Swift knows it cannot cause a cycle. It can also avoid a heap allocation.

The explicit self rule

final class ProfileViewModel {
    var name = ""

    func load() {
        loadUser { user in
            self.name = user.name      // `self.` is required
        }
    }
}

Swift forces self. so you notice that the closure holds a strong reference. If the object also holds the closure, you get a retain cycle. Fix it with a capture list:

loadUser { [weak self] user in
    guard let self else { return }
    self.name = user.name
}

For structs and enums, implicit self is allowed (Swift 5.3+), because value types do not create reference cycles.

Optional closures are always escaping

func fetch(completion: (() -> Void)?) { }   // already escaping

The closure is wrapped in Optional, which is a value that can be stored. So you cannot mark it non-escaping, and you do not need @escaping.

withoutActuallyEscaping

Rarely, you need to pass a non-escaping closure to an API that wants an escaping one, and you know it will finish in time.

func allMatch(_ items: [Int], _ test: (Int) -> Bool) -> Bool {
    withoutActuallyEscaping(test) { escapable in
        items.lazy.filter { !escapable($0) }.isEmpty
    }
}

Swift checks at run time that the closure did not really escape.

Today: async/await

Most new code uses async functions instead of escaping completion handlers.

func loadUser() async -> User { User(name: "Ana") }

let user = await loadUser()

No escaping closure, no [weak self] dance for the result. But you still see @escaping in UIKit, older SDKs and interview questions.

Common follow-up questions

  • Why did Swift make closures non-escaping by default? Most closures run right away. The default is safer and faster.
  • Is a closure stored in a property escaping? Yes. Any closure stored outside the function's scope escapes.
  • Can an inout parameter be captured by an escaping closure? No. It would let the closure change the value after the call ended.
  • Does @escaping mean the closure will always leak? No. A leak only happens if there is a cycle, for example the object stores the closure and the closure captures the object.
  • Does Task { } take an escaping closure? Yes. Its operation is stored and runs later, so it can keep captured objects alive until it finishes.