An @Async method returning CompletableFuture transfers completion or failure to a result the caller must inspect.
Spring @Async futures: make worker failure observable to the caller
The error is in the future
The checked service returns a failed future for receipt R-41. The caller obtains the managed bean from a Spring context and joins the returned future; the test observes CompletionException with the original IllegalStateException as its cause. The caller's original method invocation is not proof that delivery succeeded. A call that discards the future also discards its failure signal.
Spring's proxy handles @Async only when the call crosses that proxy. A same-class call can run without asynchronous interception. The fixture intentionally checks the managed object; the proxy lesson covers this dispatch rule. For void-returning @Async methods, failures need an uncaught-exception handler and cannot be joined by the caller in the same way.
Async work still needs ownership
An executor needs a bounded queue, rejection policy and shutdown plan. A returned future also needs a timeout or cancellation policy at its owner. None of those makes work durable: if the process exits after the HTTP response, an in-memory task can disappear. Use a committed outbox record for a delivery promise that must survive restart.
Checked source
@Async
public CompletableFuture<String> deliver(String receiptId) {
return CompletableFuture.failedFuture(
new IllegalStateException("broker unavailable: " + receiptId));
}Verification boundary
AsyncFailureContractTest.failureIsObservedThroughTheReturnedFuture runs in the downloadable Spring source kit. The excerpt omits imports and surrounding setup; the kit contains the complete tests.
Costs and limits
The test checks one failed future from a local Spring context. It does not exercise a configured bounded executor, rejection under load, cancellation, actual broker I/O or process restart.
Common Mistakes
- Do not equate method return with successful asynchronous delivery.
- Do not discard a future whose failure matters.
- Do not rely on proxy advice for a same-class method call.
Read next
Spring task executors: capacity, rejection and lost context, Spring AOP proxies: self-invocation bypasses proxy advice, Spring transactional outbox: commit a receipt and event row together.
