Handling Exceptions in Java Thread Pools: submit vs execute and Custom Strategies
This article explains why tasks submitted with ExecutorService.submit silently swallow exceptions, how ExecutorService.execute prints them, and presents three practical solutions—including try‑catch, a custom ThreadFactory with UncaughtExceptionHandler, and overriding afterExecute—to reliably capture and process thread‑pool errors in Java.
In real‑world development we often use thread pools, but when a task throws an exception after being submitted, handling it correctly becomes essential. The article first compares the two submission methods submit and execute and shows their different behaviors.
Example code demonstrates a simple thread pool where executorService.submit(new task()) does not print the exception, while executorService.execute(new task()) does:
public class ThreadPoolException {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(1);
// submit has no immediate output, other threads continue
executorService.submit(new task());
// execute throws exception, other threads continue with new task
executorService.execute(new task());
}
}
class task implements Runnable {
@Override
public void run() {
System.out.println("Entered task method!!!");
int i = 1/0; // triggers ArithmeticException
}
}The run result shows that submit does not print the exception, while execute does, making submit unsuitable for production when exceptions must be visible.
To retrieve the exception from a submit call, the article suggests using the returned Future and calling future.get() , which re‑throws the exception wrapped in an ExecutionException :
Future
submit = executorService.submit(new task());
submit.get(); // prints the exceptionThree practical solutions are then presented:
Try‑catch inside the task : wrap the task logic with a try‑catch block to handle the exception locally.
Thread.setDefaultUncaughtExceptionHandler via a custom ThreadFactory : create a ThreadFactory that sets an UncaughtExceptionHandler for each thread, allowing uncaught exceptions from execute to be logged.
Override afterExecute : extend ThreadPoolExecutor and override afterExecute(Runnable r, Throwable t) to process exceptions from both execute and submit (the latter by checking if r is an instance of FutureTask and calling future.get() ).
Code for the second solution (custom ThreadFactory) looks like:
ThreadFactory factory = (Runnable r) -> {
Thread t = new Thread(r);
t.setDefaultUncaughtExceptionHandler((Thread thread1, Throwable e) -> {
System.out.println("Thread factory exception handler: " + e.getMessage());
});
return t;
};
ExecutorService executorService = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(10), factory);
executorService.submit(new task());
executorService.execute(new task());The third solution overrides afterExecute to handle both submission types:
ExecutorService executorService = new ThreadPoolExecutor(
2, 3, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(10)) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
if (t != null) {
System.out.println("afterExecute caught execute exception: " + t.getMessage());
}
if (r instanceof FutureTask) {
try {
((Future
) r).get();
} catch (Exception e) {
System.out.println("afterExecute caught submit exception: " + e);
}
}
}
};
executorService.execute(new task());
executorService.submit(new task());In conclusion, when no result is needed, using execute (with optional try‑catch) avoids silent failures; when a result is required, submit combined with Future.get() or a custom afterExecute implementation ensures exceptions are not lost.
Architect's Tech Stack
Java backend, microservices, distributed systems, containerized programming, and more.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.