Hi Switers 🙂, In this article, we will explore withoutActuallyEscaping
function in Swift. Let’s say you have just started getting comfortable with closures in Swift. You’ve wrapped your head around things like @escaping
, and you know it’s needed when a closure might stick around and run later, after the function it was passed to has already returned. Oh wait! Lets take a quick detour.
What’s a Closure?
A closure is just a block of code you can pass around and call later. Kind of like a function but you can store it in a variable, pass it to another function, and call it later. Here is a quick example:
let sayHello = {
print("Hello!")
}
sayHello() // prints "Hello!"
Non-Escaping Closures (Default)
By default, closures in Swift are non-escaping. That means they must be used inside the function they’re passed to. Non-escaping closures are faster and safer too. A quick example would be like:
func greet(action: () -> Void) {
action() // used immediately — non-escaping
}
Escaping Closures
If the closure needs to run after the function returns like in async code or when you store it, it has to be marked @escaping
:
func doLater(action: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
action() // runs later
}
}
The Problem
Sometimes, you hit a weird edge case. You’re working with a non-escaping closure, but you want to use it in a function that requires an escaping one. Take a look at this example:
func allValues(in array: [Int], match predicate: (Int) -> Bool) -> Bool {
return array.lazy.filter { !predicate($0) }.isEmpty
}
This won’t compile. Why? Because lazy.filter
requires an escaping closure and predicate
isn’t marked @escaping
. But you know it’s safe! The lazy filter is short-lived. But then you hit this cryptic error:
“Escaping closure captures non-escaping parameter ‘predicate’”
You double-check your code. You’re not actually trying to store the closure or delay it, just passing it into something like filter
. So why’s Swift complaining? Welcome to the world of withoutActuallyEscaping(_:do:)
, a feature that sounds like a contradiction but solves a very real problem. Let’s walk through this together.
The Fix: “Without Actually Escaping”
func allValues(in array: [Int], match predicate: (Int) -> Bool) -> Bool {
return withoutActuallyEscaping(predicate) { escapablePredicate in
array.lazy.filter { !escapablePredicate($0) }.isEmpty
}
}
Now Swift is happy. What changed? the withoutActuallyEscaping
function temporarily lends an escapable copy of your closure that copy can be passed into APIs that require @escaping
. But Swift guarantees it won’t actually escape beyond that block. Another real example can be Concurrency & Dispatch Queues. Let’s say you want to run two closures at the same time:
func perform(_ f: () -> Void, simultaneouslyWith g: () -> Void) {
let queue = DispatchQueue(label: "perform", attributes: .concurrent)
queue.async(execute: f)
queue.async(execute: g)
queue.sync(flags: .barrier) {}
}
Again, Swift complains: async(execute:)
expects @escaping
. But we don’t want to mark our parameters as @escaping
unless we have to. Why? because you don’t want to promise escaping unless needed. Also avoid unintentional memory leaks. Also escaping closures need heap allocation, while non-escaping ones can live on the stack. So here is the fix using withoutActuallyEscaping
:
func perform(_ f: () -> Void, simultaneouslyWith g: () -> Void) {
withoutActuallyEscaping(f) { escapableF in
withoutActuallyEscaping(g) { escapableG in
let queue = DispatchQueue(label: "perform", attributes: .concurrent)
queue.async(execute: escapableF)
queue.async(execute: escapableG)
queue.sync(flags: .barrier) {}
}
}
}
We get the benefits of an @escaping
closure without actually making it escaping. The compiler and runtime ensure safety.
Why Is This Useful?
It helps avoid unnecessary @escaping
, keeping your APIs cleaner and safer while also improving performance by preventing heap allocations. It’s especially handy with lazy collections, concurrency, and other APIs that demand an escaping closure even when it won’t truly escape. Best of all, it remains memory-safe the escapable copy is valid only within the scope of withoutActuallyEscaping
.
If you want to dig deeper you can read more in Apple’s official documentation for withoutActuallyEscaping
.
I hope you liked this one I will see you in the next one 😉