In my codebase, I'm looking to integrate a helper function for some new async code whose general shape boils down to the following:
func setup() async { /* ... some async setup code ... */ }
func cleanup() async { /* ... some async cleanup code ... */ }
func performOperation<T>(_ op: () async -> T) async -> T {
    await setup()
    let result = await op()
    
    await cleanup()
    return result
}
(The above is highly simplified, and in reality, setup() and cleanup() aren't their own functions but inlined into performOperation.)
I'd like performOperation to be rethrows, and the following transformation (barring semantics) is easy enough:
func performOperation<T>(_ op: () async throws -> T) async -> T {
    await setup()
    let result = try await op()
    await cleanup()
    return result
}
However, I'd like to run cleanup unconditionally on op ā and crucially, I need cleanup to complete before returning from this function. In non-async code, we'd normally write this as
defer { nonAsyncCleanup() }
return try nonAsyncOp()
but this doesn't translate to async code as defer can't contain async code:
defer { await cleanup() } // error: 'async' call cannot occur in a defer body
return try await op()
Because this function is rethrows, I also can't easily separate error propagation from the original try site, e.g.
// Can't use `Result.init(catching:)` because it doesn't take an `async` func.
let result: Result<T, Error>
do {
    result = .success(try await op())
} catch {
    result = .failure(error)
}
await cleanup()
return try result.get() // error: Call can throw, but the error is not handled; a function declared 'rethrows' may only throw if its parameter does
While I'd love for the compiler to be able to reason about the above attempt, I can totally see why it doesn't allow for this.
In the end, I settled on the following:
func performOperation<T>(_ op: () async throws -> T) async rethrows -> T {
    // ... some async setup code ...
    await setup()
    
    let result: T
    do {
        result = try await op()
        await cleanup()
    } catch {
        await cleanup()
        throw error
    }
    
    return result
}
In practice, cleanup is defined as a closure inline above the do-catch, and called from both branches. I'm wondering, though:
- Is there a cleaner way to express this? I'm relatively new to async/await, so I'm happy to admit that there may be something obvious I'm missing
- Would it be reasonable to extend deferto allow runningasynccode so long as it is properlyawaited?