submit() vs execute() — what happens to an exception in e… — Cracked Java
// Concurrency & Multithreading · Executors & Thread Pools
MidTheoryTrick

submit() vs execute() — what happens to an exception in each?

execute(Runnable) returns nothing and an uncaught exception propagates to the thread's UncaughtExceptionHandler. submit(...) returns a Future and any exception is captured inside that Future — it only surfaces when you call get(). This difference is a classic source of silently swallowed errors.

The exception trap

ExecutorService pool = Executors.newFixedThreadPool(2);

// execute: exception escapes to the thread; handler/stderr sees a stack trace
pool.execute(() -> { throw new RuntimeException("boom-execute"); });

// submit: exception is swallowed into the Future, never printed
Future<?> f = pool.submit(() -> { throw new RuntimeException("boom-submit"); });
// ... and unless someone does f.get(), no one ever knows it failed
f.get();   // NOW it throws ExecutionException wrapping the RuntimeException

With execute, the throwable bubbles out of Worker.run(), the worker thread dies, and the pool's UncaughtExceptionHandler (default: print to stderr) fires. With submit, ThreadPoolExecutor wraps your task in a FutureTask whose run() catches the throwable and stores it. The worker survives; the error sits dormant in the Future. If you never call get(), the failure vanishes — no log, no alert.

API differences

  • execute(Runnable) — from Executor. Returns void. Cannot accept a Callable.
  • submit(Runnable) / submit(Callable<T>) — from ExecutorService. Returns Future<T> for the result and exception, plus cancellation.

How to avoid the swallowed exception

Always handle the result of a submit, or catch inside the task:

Future<?> f = pool.submit(task);
// later, when collecting:
try { f.get(); }
catch (ExecutionException e) { log.error("task failed", e.getCause()); }

Mark your status