How do you customize what happens to uncaught exceptions from void @Async methods? Walk through AsyncConfigurer and AsyncUncaughtExceptionHandler.
answer
- AsyncConfigurer.getAsyncUncaughtExceptionHandler()
- handleUncaughtException(ex, method, params)
- default = SimpleAsyncUncaughtExceptionHandler (log ERROR)
- void only — futures capture their own error
- runs on worker thread, no caller MDC/SecurityContext
basics
~10 sImplement AsyncConfigurer and override getAsyncUncaughtExceptionHandler() to return your own AsyncUncaughtExceptionHandler. Its handleUncaughtException(throwable, method, params) method runs whenever a void @Async method throws, so you can alert, record, or retry instead of just logging.
solid answer
~40 sFor void @Async methods there is no future to carry the error, so Spring delegates uncaught exceptions to an AsyncUncaughtExceptionHandler. To customize it, provide an AsyncConfigurer (usually on your @Configuration @EnableAsync class) and override getAsyncUncaughtExceptionHandler() to return your implementation. That interface has one method: handleUncaughtException(Throwable ex, Method method, Object... params) — Spring passes the failing method and the actual argument values, so you can log context, publish a metric, send an alert, or write a dead-letter record. If you don't override it, Spring uses SimpleAsyncUncaughtExceptionHandler, which only logs at ERROR. Note this handler applies ONLY to void return types; methods returning Future/CompletableFuture capture the exception in the future instead and never invoke this handler. Also override getAsyncExecutor() in the same AsyncConfigurer if you want a custom executor.
code
java · 29 lines@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
private static final Logger log = LoggerFactory.getLogger(AsyncConfig.class);
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(4);
exec.setThreadNamePrefix("async-");
exec.initialize();
return exec;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new CustomAsyncExceptionHandler();
}
static class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
log.error("Uncaught async error in {} args={}",
method.getName(), Arrays.toString(params), ex);
// e.g. publish metric, send alert, write dead-letter row
}
}
}go deeper
Aware a custom handler exists and the default just logs.
Can wire AsyncConfigurer + AsyncUncaughtExceptionHandler and knows it's void-only.
Adds context propagation and observability (metrics/alerts/dead-letter) considerations.
Designs a coherent async failure policy: handler + executor + rejection policy + propagation across the platform.
## Why a special handler exists When a `@Async` method returns a `Future`/`CompletableFuture`, any exception is stored **in the future** and surfaced via `get()`/`exceptionally()`. But when the method returns **void**, there is nowhere to store the failure — the caller already moved on. Spring solves this by routing such uncaught exceptions to a single strategy interface: **`AsyncUncaughtExceptionHandler`**. ## The interface ```java public interface AsyncUncaughtExceptionHandler { void handleUncaughtException(Throwable ex, Method method, Object... params); } ``` - `ex` — the throwable that escaped the async method. - `method` — the reflective `java.lang.reflect.Method` that failed (name, declaring class, etc.). - `params` — the actual argument values passed to that invocation, useful for context/correlation. ## The default If you register nothing, Spring uses **`SimpleAsyncUncaughtExceptionHandler`**, whose entire job is to **log the exception at ERROR level** (including the method signature). Nothing else — no metric, no alert, no persistence. ## How to plug in your own — `AsyncConfigurer` `AsyncConfigurer` is a callback interface for configuring Spring's async infrastructure. It has two methods, both with default (null-returning) implementations so you override only what you need: ```java public interface AsyncConfigurer { default Executor getAsyncExecutor() { return null; } default AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return null; } } ``` Returning `null` means "use Spring's default". So to customize just the handler, implement `AsyncConfigurer` on your `@EnableAsync @Configuration` class and override `getAsyncUncaughtExceptionHandler()`. ```java @Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return (ex, method, params) -> { log.error("Async {} failed with args {}", method.getName(), Arrays.toString(params), ex); metrics.counter("async.errors", "method", method.getName()).increment(); }; } } ``` ## Critical scoping rule The handler is invoked **only for void-returning `@Async` methods**. If the method returns `Future`/`CompletableFuture`, Spring captures the exception inside the future and **never calls the handler** — the caller is expected to inspect the future. Mixing this up is a common interview trap. ## Gotchas - Prefer implementing `AsyncConfigurer` (or extending `AsyncConfigurerSupport`, though that's deprecated in newer Spring — `AsyncConfigurer` now has default methods) over separate bean definitions, so both executor and handler are configured coherently. - The handler runs on the **async worker thread**, not the caller thread — don't assume request-scoped beans / SecurityContext / MDC are present unless you propagated them. - Only **uncaught** exceptions arrive here; anything caught inside the method never reaches it. - One handler applies globally to all void `@Async` methods in the context. ## When to use - Fire-and-forget tasks (emails, notifications, audit writes) where you still need observability, alerting, dead-lettering, or metrics on failure — exactly what the default log-only handler can't give you.
- Does this handler fire for a @Async method returning CompletableFuture that throws?No. Only void @Async methods use the AsyncUncaughtExceptionHandler. For future-returning methods the exception is stored in the future and delivered via get()/exceptionally()/handle(); the handler is never invoked.
- What are the arguments to handleUncaughtException and why are they useful?Throwable ex (the failure), java.lang.reflect.Method method (the failing method — name, class), and Object... params (the actual call arguments). params/method let you build contextual log messages, correlation IDs, metrics tags, or dead-letter records.
- Are request-scoped values like SecurityContext or MDC available inside the handler?Not automatically — it runs on the async worker thread. You must propagate them (e.g. a TaskDecorator copying MDC/SecurityContext) if you need them in the handler or the async method.
saying these in an interview costs you the question
- Thinking getAsyncUncaughtExceptionHandler also handles Future/CompletableFuture failures
- Believing you must annotate each method with the handler
- Assuming SecurityContext/MDC are present on the async thread
- Confusing AsyncUncaughtExceptionHandler with the generic Thread.UncaughtExceptionHandler