Hard java spring

An async task throws, but nothing is logged and the job just stops?

The 30 second answer

The exception was captured, not lost. With submit, the executor stores it in the returned Future and only get rethrows it. With execute, it reaches the thread's uncaught exception handler. CompletableFuture keeps it until someone calls join or get, and join wraps it in CompletionException. I always observe the result, or attach a handler, and log the cause at one place.

What happens. It depends on how the task was started.

  • **submit()** wraps the task in a FutureTask, which catches the exception and stores it. Nothing is printed. Only Future.get() rethrows it, as ExecutionException with the original as the cause. If no one calls get, the failure is invisible. ThreadPoolExecutor.afterExecute also receives a null throwable for such tasks; its Javadoc shows how to unwrap the Future to find it.
  • **execute()** lets the exception escape the task, so the worker thread dies and the thread's UncaughtExceptionHandler runs, the default one prints the stack trace to standard error. The pool replaces the thread.
  • **CompletableFuture** completes exceptionally. join() throws CompletionException, get() throws ExecutionException, and in both the real error is getCause(). Logging only the wrapper's message hides it.
  • Scheduled tasks. A periodic task from scheduleAtFixedRate that throws is not run again, which is why "the job just stops".
import java.util.concurrent.*;

public class SilentFailure {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        // 1. submit(): the exception is stored in the Future; nothing is printed
        Future<?> f = pool.submit(() -> { throw new IllegalStateException("lost in submit"); });
        Thread.sleep(100);
        try { f.get(); } catch (ExecutionException e) {
            System.out.println("get() -> " + e.getCause());
        }

        // 2. execute(): the exception reaches the thread's UncaughtExceptionHandler
        ExecutorService logged = Executors.newSingleThreadExecutor(r -> {
            Thread t = new Thread(r);
            t.setUncaughtExceptionHandler((th, ex) -> System.out.println("handler -> " + ex));
            return t;
        });
        logged.execute(() -> { throw new IllegalStateException("seen in execute"); });
        Thread.sleep(100);

        // 3. CompletableFuture: join() wraps the cause in CompletionException
        CompletableFuture<Integer> cf = CompletableFuture.supplyAsync(() -> 1 / 0, pool);
        try { cf.join(); } catch (CompletionException e) {
            System.out.println("join() -> " + e.getCause());
        }
        pool.shutdown(); logged.shutdown();
    }
}

Verified with OpenJDK 21.0.13. It prints get() -> java.lang.IllegalStateException: lost in submit, handler -> java.lang.IllegalStateException: seen in execute and join() -> java.lang.ArithmeticException: / by zero.

Fixes.

  • Always consume the Future, or chain whenComplete or exceptionally on a CompletableFuture and log the unwrapped cause there.
  • Catch and log inside periodic tasks so one failure does not cancel the schedule.
  • Set a thread factory with an UncaughtExceptionHandler for execute-style pools, and override afterExecute if the pool must log both kinds.
  • In streams, a lambda that throws aborts the whole pipeline; wrapping checked exceptions is fine, but catching and returning null silently drops data.
  • In Spring, @Async methods returning void send errors to an AsyncUncaughtExceptionHandler; methods returning a future put them in the future.

Alternate interview scenarios

"A parallel stream or CompletableFuture chain fails, and the log only says CompletionException." Unwrap: log e.getCause(), or use exceptionally(ex -> ...) where ex may itself be a CompletionException. Decide per call whether one failed element should fail the whole result or be recorded and skipped.

Sources (5)
  1. ExecutorService Javadoc (Java 21): submit returns a Future
  2. ThreadPoolExecutor Javadoc (Java 21): afterExecute and FutureTask
  3. Thread.UncaughtExceptionHandler Javadoc (Java 21)
  4. CompletableFuture Javadoc (Java 21): join and CompletionException
  5. ScheduledExecutorService Javadoc (Java 21): a failing periodic task is suppressed