How to Handle Exceptions Thrown by Threads in a ThreadPool

This article explains why tasks submitted to a Java ThreadPool via submit silently swallow exceptions while execute prints them, and demonstrates three practical ways—using Future.get(), a custom UncaughtExceptionHandler, and overriding afterExecute—to reliably capture and process those exceptions.

Java Backend Full-Stack
Java Backend Full-Stack
Java Backend Full-Stack
How to Handle Exceptions Thrown by Threads in a ThreadPool

In Java development, thread pools are frequently used, but when a task throws an exception the handling differs between submit and execute. A simple demo shows that submit does not print the exception, whereas execute does.

public class ThreadPoolException {
    public static void main(String[] args) {
        // create a thread pool with a single thread
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        // submit: no immediate exception output, other threads continue
        executorService.submit(new task());
        // execute: exception is printed, other threads continue with new tasks
        executorService.execute(new task());
    }
}

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

The submit call returns a Future. Calling future.get() re‑throws the exception, while execute lets the exception propagate to the thread’s default handler.

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("Entering task method!!!");
            int i = 1 / 0;
        } catch (Exception e) {
            System.out.println("Caught exception with try‑catch: " + e);
        }
    }
}

This approach works for both submit and execute because each task handles its own exception.

Solution 2: Set a default UncaughtExceptionHandler via a custom ThreadFactory

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

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

Here the handler catches exceptions from execute but not from submit, because FutureTask swallows the exception internally.

Solution 3: Override afterExecute in a custom ThreadPoolExecutor

public class ThreadPoolException3 {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        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());
    }
}

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

Overriding afterExecute allows handling of exceptions from both execute (directly via the Throwable argument) and submit (by checking if the runnable is a FutureTask and invoking get()).

Why submit Hides the Exception

The source of the difference lies in the implementation: submit creates a FutureTask that wraps the original Runnable. Its run() method catches any Throwable and stores it via setException(ex). The exception is later re‑thrown only when Future.get() is called, which explains why the thread’s UncaughtExceptionHandler is not invoked for submit.

public void run() {
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran) set(result);
        }
    }
    // ...
}

private V report(int s) throws ExecutionException {
    Object x = outcome;
    if (s == NORMAL) return (V) x;
    if (s >= CANCELLED) throw new CancellationException();
    throw new ExecutionException((Throwable) x);
}

Thus, to retrieve the exception from a submit task you must call Future.get() or implement a custom afterExecute that performs this call.

submit no exception output
submit no exception output
submit exception via get()
submit exception via get()
execute exception caught by ThreadFactory
execute exception caught by ThreadFactory
afterExecute handling both cases
afterExecute handling both cases
JavaThreadPoolExecutorServiceFutureExceptionHandlingafterExecute
Java Backend Full-Stack
Written by

Java Backend Full-Stack

Provides technical guidance, interview coaching, and tech sharing. Follow and reply '77' to receive our self-made 'Interview Cheat Sheet' and interview resources.

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.