Why Does submit() Hide Exceptions in Java Thread Pools? Uncover the Truth

This article explains why tasks submitted with submit() in a Java thread pool silently swallow exceptions, contrasts it with execute(), and presents three practical solutions—including try‑catch, a default UncaughtExceptionHandler, and overriding afterExecute—to reliably capture and handle those errors.

Java Backend Technology
Java Backend Technology
Java Backend Technology
Why Does submit() Hide Exceptions in Java Thread Pools? Uncover the Truth

Problem Overview

In real development we often use thread pools; when a task throws an exception, how can we handle it and retrieve the exception information?

Demonstration Code

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

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

Running the code shows that submit() does not print the exception while execute() does.

Solution 1: try‑catch inside task

Use a try‑catch block in the task's run() method to capture the exception.
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("enter task method!!!");
            int i = 1 / 0;
        } catch (Exception e) {
            System.out.println("caught exception via try‑catch: " + e);
        }
    }
}

Both submit and execute now clearly capture the exception.

Solution 2: Thread.setDefaultUncaughtExceptionHandler

Set a default uncaught exception handler for all threads to catch exceptions that escape the task.
ThreadFactory factory = (Runnable r) -> {
    Thread t = new Thread(r);
    t.setDefaultUncaughtExceptionHandler((thread, 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
Thread.sleep(1000);
System.out.println("=== after 1s, execute ===");
executorService.execute(new task()); // handler prints exception

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

Solution 3: Override afterExecute in ThreadPoolExecutor

Override afterExecute to process exceptions for both execute and submit submissions.
ExecutorService executor = new ThreadPoolExecutor(2, 3, 0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<>()) {
    @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.getMessage());
            }
        }
    }
};

executor.execute(new task());
executor.submit(new task());

The overridden method prints exception information for both execution paths.

Source Code Analysis

The submit() method creates a FutureTask, calls execute(ftask), and stores any thrown exception inside the FutureTask 's internal outcome field via setException(ex). When Future.get() is invoked, it calls report(s), which re‑throws the stored exception as an ExecutionException. Therefore, submit does not print the exception directly, but the exception can be retrieved through the returned Future.

In contrast, execute() runs the task directly; any RuntimeException propagates up to the thread pool’s afterExecute hook, where it can be observed.

Consequently, to reliably capture exceptions you can either use execute with a custom afterExecute implementation, or use submit and retrieve the exception via Future.get().

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.

JavaException HandlingExecutorServicethread-pool
Java Backend Technology
Written by

Java Backend Technology

Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!

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.