Handling Exceptions in Java Thread Pools: submit vs execute and Custom afterExecute

This article explains why tasks submitted to a Java thread pool via submit do not print exceptions, how to retrieve those exceptions using Future.get, and presents three practical solutions—including try‑catch, Thread.setDefaultUncaughtExceptionHandler, and overriding afterExecute—to reliably capture and process thread‑pool errors.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Handling Exceptions in Java Thread Pools: submit vs execute and Custom afterExecute

1. Simulating Thread‑Pool Exceptions

In real projects we often use a thread pool, but when a task throws an exception we need to know how to capture it. First we simulate the situation with the following code:

public class ThreadPoolException {
    public static void main(String[] args) {
        // create a thread pool with a single thread
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        // submit does not show the exception, other threads continue
        executorService.submit(new task());
        // execute throws the exception, other threads continue with new tasks
        executorService.execute(new task());
    }
}

// task class
class task implements Runnable {
    @Override
    public void run() {
        System.out.println("Entered task method!!!");
        int i = 1 / 0; // will cause ArithmeticException
    }
}

Running the program shows that execute prints the exception while submit silently ignores it.

To obtain the exception from a submit call you must call Future.get() on the returned Future:

Future<?> submit = executorService.submit(new task());
submit.get(); // prints the exception

2. How to Get and Process the Exception

Solution 1: Use try‑catch inside the task

public class ThreadPoolException {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.submit(new task());
        executorService.execute(new task());
    }
}

class task implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println("Entered task method!!!");
            int i = 1 / 0;
        } catch (Exception e) {
            System.out.println("Caught exception with try‑catch: " + e);
        }
    }
}

Both submit and execute now show the exception because the task itself handles it.

Solution 2: Use Thread.setDefaultUncaughtExceptionHandler

Adding a global uncaught‑exception handler to every thread created by the pool avoids having to write try‑catch in each task:

ThreadFactory factory = (Runnable r) -> {
    Thread t = new Thread(r);
    t.setDefaultUncaughtExceptionHandler((Thread thread, Throwable e) -> {
        System.out.println("Thread factory handler: " + e.getMessage());
    });
    return t;
};

ExecutorService executorService = new ThreadPoolExecutor(
        1, 1, 0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<>(10), factory);

executorService.submit(new task()); // no output – submit swallows the exception
Thread.sleep(1000);
System.out.println("=== after 1s, execute ===");
executorService.execute(new task()); // handler prints the exception

The handler receives exceptions from execute but not from submit because submit wraps the task in a FutureTask that catches the exception internally.

Solution 3: Override afterExecute in a custom ThreadPoolExecutor

By overriding afterExecute(Runnable r, Throwable t) we can process exceptions for both submission methods. For submit we need to check whether the runnable is a FutureTask and call get() to retrieve the stored exception.

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());

Running this shows that afterExecute can handle exceptions from both execute and submit submissions.

Key Takeaways

When a task throws an exception, execute propagates it to the thread’s uncaught‑exception mechanism, while submit captures it inside a FutureTask.

To see the exception from submit, call Future.get() or override afterExecute and inspect the FutureTask.

Three practical ways to handle exceptions: embed try‑catch in the task, set a global UncaughtExceptionHandler via a custom thread factory, or override afterExecute in a custom ThreadPoolExecutor.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ThreadPoolExecutorServiceexceptionhandling
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.