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
defer
to allow runningasync
code so long as it is properlyawait
ed?