You are designing a reusable await() bridge for a Future/CompletableFuture-style API. What correctness concerns must the implementation address?
answer
- Resume once from completion handler
- Unwrap ExecutionException/CompletionException cause
- invokeOnCancellation { future.cancel(true) }
- Fast path when isDone
- Release a value that loses to cancellation
basics
~10 sResume once from the completion handler with the result or the exception, cancel the underlying future in invokeOnCancellation, unwrap wrapper exceptions, and make sure a result arriving after cancellation does not leak.
solid answer
~40 sA robust await() over a future-like API must: register a completion handler that calls resume / resumeWithException exactly once; unwrap framework wrapper exceptions (e.g. ExecutionException/CompletionException) so callers see the real cause; cancel the underlying operation in invokeOnCancellation so a cancelled coroutine does not keep the future alive; and decide whether cancellation should also cancel the future (often yes, mayInterruptIfRunning). It must handle a result/error that races with cancellation — use the resume(value, onCancellation) overload or isActive guard to avoid leaks. If the future is already complete when await() is called, resume synchronously without registering needless callbacks. Keep invokeOnCancellation non-blocking and idempotent. kotlinx.coroutines ships future.await() for CompletableFuture/ListenableFuture, but writing one demonstrates the concerns.
go deeper
Can sketch resume/resumeWithException from a completion handler.
Adds invokeOnCancellation to cancel the future and handles success/error.
Unwraps wrapper exceptions, handles the cancellation race and fast path, keeps cleanup safe.
Decides cancellation propagation policy, shared-future ownership, and prefers/justifies the library implementation.
## The shape of the bridge ```kotlin suspend fun <T> CompletableFuture<T>.awaitX(): T = suspendCancellableCoroutine { cont -> // already done? resume now if (isDone) { try { cont.resume(get()) } catch (e: ExecutionException) { cont.resumeWithException(e.cause ?: e) } return@suspendCancellableCoroutine } whenComplete { value, err -> if (err == null) cont.resume(value) else cont.resumeWithException( (err as? CompletionException)?.cause ?: err ) } cont.invokeOnCancellation { cancel(true) } } ``` ## Concerns to address ### 1. Exactly-once resume The completion handler must resume once. `whenComplete` fires once, but if you also handle the already-done case guard against double paths (the example returns early). ### 2. Exception unwrapping Futures wrap the real failure: `CompletableFuture` reports `CompletionException`, blocking `get()` throws `ExecutionException`. Unwrap with `?.cause` so the caller catches the **domain** exception, not a framework wrapper. ### 3. Cancel the underlying future `invokeOnCancellation { cancel(true) }` propagates coroutine cancellation downstream so the future does not keep running. Decide on `mayInterruptIfRunning` (`true` usually). ### 4. Result-vs-cancellation race A value may arrive right after cancellation. If the value owns resources, release it: `cont.resume(value) { value.close() }`. Otherwise the dropped value is harmless. ### 5. Fast path when already complete If `isDone`, resume synchronously to avoid an unnecessary callback registration. ### 6. Non-blocking, idempotent cleanup `invokeOnCancellation` runs in an undefined context — never block or throw there; `cancel(true)` on an already-completed future is a harmless no-op (idempotent). ### 7. CancellationException is special Don't accidentally wrap a `CancellationException` from the future as a normal error — let cancellation semantics flow. ## What the standard library does kotlinx.coroutines provides `CompletableFuture<T>.await()` and `ListenableFuture<T>.await()` (kotlinx-coroutines-jdk8 / -guava) implementing exactly these concerns — prefer them in production; hand-rolling is for cases without library support.
- Why unwrap CompletionException before resumeWithException?So callers catch the real domain cause rather than the framework wrapper, matching how a direct suspend call would throw.
- Should coroutine cancellation cancel the future?Usually yes, via invokeOnCancellation { cancel(true) }, to honor structured concurrency and stop wasted work, unless the future is shared.
saying these in an interview costs you the question
- Forgets to cancel the underlying future on cancellation
- Exposes ExecutionException/CompletionException wrappers to callers
- Blocks (calls get()) instead of registering whenComplete
- Ignores the already-complete fast path
- Reinvents await() instead of using the kotlinx integration when available