What is an escaping closure in Swift?
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-escaping | Escaping | |
|---|---|---|
| Runs after function returns | No | Yes |
| Can be stored | No | Yes |
Implicit self in classes | Allowed | You must write self. or capture it |
| Retain cycle risk | No | Yes |
| Optimization | Context can stay on the stack | Context 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 escapingThe 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
inoutparameter be captured by an escaping closure? No. It would let the closure change the value after the call ended. - Does
@escapingmean 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.