skip to content

How do you customize what happens to uncaught exceptions from void @Async methods? Walk through AsyncConfigurer and AsyncUncaughtExceptionHandler.

level: middleimportance: must knowfreq 65%

answer

  1. AsyncConfigurer.getAsyncUncaughtExceptionHandler()
  2. handleUncaughtException(ex, method, params)
  3. default = SimpleAsyncUncaughtExceptionHandler (log ERROR)
  4. void only — futures capture their own error
  5. runs on worker thread, no caller MDC/SecurityContext

basics

~10 s

Implement 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 s

For 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
java
@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

for a junior

Aware a custom handler exists and the default just logs.

for a middle

Can wire AsyncConfigurer + AsyncUncaughtExceptionHandler and knows it's void-only.

for a senior

Adds context propagation and observability (metrics/alerts/dead-letter) considerations.

for a principal

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

context