Swift’s withoutActuallyEscaping: Escape Without Escaping?

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:

Swift
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:

Swift
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:

Swift
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:

Swift
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”

Swift
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:

Swift
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:

Swift
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.

I hope you liked this one I will see you in the next one 😉


Posted

in

Tags: