Compiler issue: Escaping closure captures non-escaping parameter 'block'

How can we explain why func executeNOK doesn’t compile but execute1 and executeOk does?

    func executeOk(block: () -> Void) {
        { block() }()
    }

    func execute1(block: () -> Void) {
        let closure = { block() }
    }

    func executeNOK(block: () -> Void) {
        let closure = { block() } // compiler issue: Escaping closure captures non-escaping parameter 'block'
        closure()
    }

Xcode Toolchain 26.1, Swift 6.2.1

1 Like

my best current guess:

func executeOk(block: () -> Void) {
    { block() }()
}

this one compiles because the wrapping closure is also inferred to be non-escaping. this is visible in the AST dump output (see this, and note how there is no escaping tag on the corresponding closure_expr).

 func execute1(block: () -> Void) {
    let closure = { block() }
}

since the closure is stored in this case, the closure's function type is escaping (variables like this currently are always escaping AFAIK – see discussion here). but this compiles because the diagnostics check for an 'interesting' use of the escaped value, and there aren't any here.

func executeNOK(block: () -> Void) {
    let closure = { block() } // compiler issue: Escaping closure captures non-escaping parameter 'block'
    closure()
}

this one is similar to execute1 but fails to compile because there is a use of the escaping closure that captures the non-escaping parameter. that's treated as a violation of the escaping rules even though in your example it's clearly not actually escaping.


IIUC, the diagnostic you hit in the final case is implemented in DiagnoseInvalidEscapingCaptures.cpp if you're interested in the particulars. @Slava_Pestov will hopefully correct any errors in this assessment.

1 Like

ah, and a thing i forgot to mention – if you do want to get cases like executeNOK to build, you can use withoutActuallyEscaping to temporarily treat the non-escaping closure as if it were escaping:

func executeNOK(block: () -> Void) {
  withoutActuallyEscaping(block) { block in
    let closure = { block() } // ✅
    closure()
  }
}

however, it's then up to you to actually ensure that condition is upheld. as the docs say:

The escapable copy of closure passed to body is only valid during the call to withoutActuallyEscaping(_:do:) . It is undefined behavior for the escapable closure to be stored, referenced, or executed after the function returns.

2 Likes

Thank you @jamieQ for the link to withoutActuallyEscaping. My code which lead me to the question on the forum, was exactly as the second example of code snippet in documentation of the withoutActuallyEscaping.

I have tested these variants with withoutActuallyEscaping to get an idea how it works:

    func execute(block: () -> Void) {
        withoutActuallyEscaping(block) { block in
            myExecuteLaterX(block)
        }
    }

    func myExecuteLater1(_ block: @escaping () -> ()) {
        block()
    }

    func myExecuteLater2(_ block: @escaping () -> ()) {
        sleep(2)
        block()
    }

    func myExecuteLater3(_ block: @escaping () -> ()) {
        DispatchQueue.main.async { block() }
    }

    func myExecuteLater4(_ block: @escaping () -> ()) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 100) {
            block()
        }
    }

myExecuteLater1 and 2 execute without crashing.
myExecuteLater3 crashed at runtime. I tested myExecuteLater4 to check whether the crash happens when passing the closure to the @escaping argument or when the closure is executed later (just to confirm the crash message). The result is that it crashes immediately, at the moment the closure is passed as an argument.

The crash message is:

The crash is: "Thread 1: closure argument was escaped in withoutActuallyEscaping block:"

Now, why do we need withoutActuallyEscaping?

withoutActuallyEscaping is essentially a hack — an unsafe, developer-driven manual decision that can have consequences at runtime.

It seems like the compiler could resolve this automatically, but implementing the logic to determine whether an escaping closure ultimately behaves as non-escaping would require too much complexity. Am I right?

IIRC from looking into this in the past, i think there is a runtime check/assert of the refcount of the closure argument at exit of the withoutActuallyEscaping function, so i would guess that's the point at which you'd generally expect to see a runtime error (though with optimizations on, idk maybe it moves around or isn't 100% reliable).

as i understand it, it's just a tradeoff. in your initial example, the compiler's analysis is conservative and it won't let you express in-actuality-safe code. rather than that just being the end of the story, Swift gives you an out, but it's a 'Spider Man' sort of deal – you take on the responsibility of ensuring things don't escape (well, the runtime tries to 'help' you too, by crashing if you've failed to uphold your end of the bargain).

i would expect as the ownership model and stuff like ~Escapable types becomes more fully built-out, we will get more precise control over this sort of thing. e.g. if you could just declare that your local variable is ~Escapable in some manner, then i think your initial example would work as desired without need for 'opting out' of the static safety checks. mabye this is possible today and i'm just unaware of the syntax you'd need to do it? sounds like an somewhat entertaining exercise for the reader :slight_smile:

1 Like

This was my exact thought. With the new ownership system, we should be able to more precisely check the validity of such code. And my guess is, that we will see exactly these improvements in future versions, if we look at the evolution of Sendable checking.

1 Like

Here's a seven year old blog post I wrote, back when I thought I might write a blog:

It provides at least one answer for why one might want this escape hatch.

@sveinhal, I tried your code, and I didn’t get the same behaviour. I also tested older Swift versions (your post was written in 2018), such as 4.2.4, to see whether the compiler behaviour had changed, but the result was the same.

I created an example intended to reproduce the behaviour described in your blog post:

func myFunc(_ predicate: () -> Void) -> Bool {
  _ = predicate()
  print("non escaping function signature")
  return true
}

func myFunc(_ predicate: @escaping () -> Void) -> String {
  _ = predicate()
  print("escaping function signature")
  return ""
}

func test() {
    withoutActuallyEscaping({}) { _ = myFunc($0) }
    _ = myFunc({})
}

test()

After reading your blog, I was expecting the following output:

"escaping function signature"
"non escaping function signature"

But I got:

"escaping function signature"
"escaping function signature"

Here is a SwiftFiddle playground with the code sample: SwiftFiddle - Swift Online Playground

func myFunc(_ predicate: () -> Void) -> Bool {
    _ = predicate()
    print("non escaping function signature")
    return true
}

func myFunc(_ predicate: @escaping () -> Void) -> String {
    _ = predicate()
    print("escaping function signature")
    return ""
}

func test(_ nonEscapingClosure: () -> Void) {
    withoutActuallyEscaping(nonEscapingClosure) { _ = myFunc($0) }
    _ = myFunc(nonEscapingClosure)
}

test({})

outputs

escaping function signature
non escaping function signature
1 Like
extension Sequence {
    var first: Element? {
        var iter = makeIterator()
        return iter.next()
    }
    func firstMatch(_ predicate: (Element) -> Bool) -> Element? {
        self.lazy.filter(predicate).first
    }
    func firstMatchWithoutActuallyEscaping(_ predicate: (Element) -> Bool) -> Element? {
        withoutActuallyEscaping(predicate) { escapingClosure in
            self.lazy.filter(escapingClosure).first
        }
    }
}

let _ = (0..<1_000_000).firstMatch { candidate in
    print("evaluating \(candidate)")
    return true
}

prints:

evaluating 0
evaluating 1
evaluating 2
evaluating 3
...
evaluating 999999
let _ = (0..<1_000_000).firstMatchWithoutActuallyEscaping { candidate in
    print("evaluating \(candidate)")
    return true
}

prints

evaluating 0

Thank you for the example. In your blog post, you used compactMap. Is there any particular reason you switched to filter?

1 Like

With the new languages features like ~Copyable or ~Escapable could this be improved upon to cause a compilation error if user attempts to do anything like that?

maybe? though this API strikes me as a carve out for circumstances in which you can't easily (or possibly ever) encode the true nature of 'non-escaping-ness' into the type system, but nothing actually escapes at runtime. IMO the dispatch examples from the docs are a good illustration – if you're submitting things async to a concurrent queue, the work item blocks are escaping – there's no real way around that. but if you know you will wait on a barrier block before returning, they're not going to escape the caller's execution. not sure there's really a way for the compiler to necessarily be able to determine that sort of thing statically.

2 Likes

I wrote that several years ago, and I think it was based on a concrete use case from these forums. However, when I just checked in a playground-doc just now, I used filter just for simplicity.

I'm not sure what has changes in collection/sequence-implementation in the standard library, or in the escaping-analysis in the compiler, since 2018.

One problem I encountered now, was that first isn't available on Sequence, so calling first (without the implementation I provided), caused the type resolver to instead choose a filter that returned a Collection. That is, the [Element]-returning one was chosen, again negating the .lazy.

1 Like