Backend Development 14 min read

Handling Exceptions in Java ThreadPool: submit vs execute and Custom Solutions

This article explains why exceptions submitted to a Java thread pool via submit are silent, how to retrieve them using Future.get(), compares submit and execute behaviors, and presents three practical solutions—including try‑catch, Thread.setDefaultUncaughtExceptionHandler, and overriding afterExecute—to reliably capture and process thread pool errors.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Handling Exceptions in Java ThreadPool: submit vs execute and Custom Solutions

In real‑world development we often use thread pools, but when a task throws an exception after being submitted, how can we handle it and retrieve the exception information? Before answering, we look at the thread‑pool source to understand the difference between submit and execute .

We first simulate an exception scenario with pseudo‑code:

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

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

Running the code shows that submit does not print the exception while execute does, which is undesirable in production because we cannot guarantee tasks never fail.

To obtain the exception from a submit call we must use Future.get() :

Future
submit = executorService.submit(new task());
submit.get();

Result: Future.get() prints the exception.

Solution 1: Use try‑catch

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 clearly capture the exception.

Solution 2: Use Thread.setDefaultUncaughtExceptionHandler

Adding a global uncaught‑exception handler via a custom thread factory avoids sprinkling try‑catch blocks in every task:

public class ThreadPoolException {
    public static void main(String[] args) throws InterruptedException {
        ThreadFactory factory = (Runnable r) -> {
            Thread t = new Thread(r);
            t.setDefaultUncaughtExceptionHandler((Thread thread1, Throwable e) -> {
                System.out.println("ThreadFactory exceptionHandler: " + e.getMessage());
            });
            return t;
        };
        ExecutorService executorService = new ThreadPoolExecutor(
                1, 1, 0, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue(10), factory);
        executorService.submit(new task());
        Thread.sleep(1000);
        System.out.println("=== after 1s, execute ===");
        executorService.execute(new task());
    }
}

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

The custom handler catches exceptions from execute but not from submit , because submit wraps the task in a FutureTask that swallows the exception.

Solution 3: Override afterExecute

By overriding afterExecute in a custom ThreadPoolExecutor we can process exceptions for both execute and submit submissions:

public class ThreadPoolException3 {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executorService = new ThreadPoolExecutor(
                2, 3, 0, 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());
    }
}

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

The overridden afterExecute prints exception information for both submission methods.

In summary, when a task may throw an exception, using execute with a global handler or overriding afterExecute is preferable; if submit is required, retrieve the exception via Future.get() or handle it inside afterExecute .

Finally, the author encourages readers to like, share, and follow the public account for more technical content.

ConcurrencyException HandlingthreadpoolExecutorServiceFutureafterexecute
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

login 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.