Java
ExecutorService
exception handling
concurrent programming
multithreading

Handling exceptions from Java ExecutorService tasks

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

In Java, the ExecutorService framework provides a high-level concurrency API for managing multiple threads. It simplifies parallel execution by abstracting thread management details. However, a pivotal aspect of leveraging this framework is adeptly handling exceptions from tasks submitted to an ExecutorService.

Understanding Exception Handling in ExecutorService

When a task (or Callable) is executed in an ExecutorService, exceptions may occur. Unlike traditional single-threaded applications, handling these exceptions in a multithreaded environment requires special attention. Exceptions in a task do not surface on the main thread; instead, they remain encapsulated within the worker threads unless explicitly handled.

Key Concepts

  • Runnable vs. Callable:
    • Runnable does not return a result and cannot throw checked exceptions.
    • Callable returns a result and can throw checked exceptions.
  • Future: When a task is submitted, it returns a Future object that acts as a handle for the task. It provides methods to check task completion (isDone()), wait for the task result (get()), and cancel the task (cancel()).

Handling Exceptions with Future

Using Future, you can catch exceptions by calling the get() method, which will throw an ExecutionException if the task aborted due to an exception.

Example

java
1ExecutorService executor = Executors.newFixedThreadPool(2);
2
3Callable<Integer> task = () -> {
4    if (Math.random() > 0.5) {
5        throw new RuntimeException("Failed task");
6    }
7    return 42;
8};
9
10Future<Integer> future = executor.submit(task);
11
12try {
13    Integer result = future.get();
14} catch (ExecutionException e) {
15    Throwable cause = e.getCause();
16    System.out.println("Exception in task: " + cause.getMessage());
17} catch (InterruptedException e) {
18    Thread.currentThread().interrupt(); // Preserve interrupt status
19    System.out.println("Task was interrupted");
20} finally {
21    executor.shutdown();
22}

Exception Handling Strategies

  1. Immediate Attention with get(): Use future.get() to retrieve the result, which blocks until the result is available or an exception is thrown.
  2. Collecting Exceptions for Multiple Tasks: When handling multiple futures, you can aggregate exceptions and handle them collectively.

Example for Multiple Futures

java
1List<Callable<Integer>> tasks = List.of(Task1, Task2, Task3);
2List<Future<Integer>> futures = executor.invokeAll(tasks);
3
4for (Future<Integer> future : futures) {
5    try {
6        Integer result = future.get();
7    } catch (ExecutionException e) {
8        System.out.println("Task exception: " + e.getCause());
9    } catch (InterruptedException e) {
10        Thread.currentThread().interrupt();
11    }
12}

Custom Uncaught Exception Handler

Use the ThreadPoolExecutor and override the afterExecute method to define a custom way of handling exceptions.

Example

java
1class CustomExecutor extends ThreadPoolExecutor {
2    public CustomExecutor() {
3        super(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
4    }
5
6    @Override
7    protected void afterExecute(Runnable r, Throwable t) {
8        super.afterExecute(r, t);
9        if (t == null && r instanceof Future<?>) {
10            try {
11                Object result = ((Future<?>) r).get();
12            } catch (CancellationException ce) {
13                t = ce;
14            } catch (ExecutionException ee) {
15                t = ee.getCause();
16            } catch (InterruptedException ie) {
17                Thread.currentThread().interrupt(); // Ignore interrupted status
18            }
19        }
20        
21        if (t != null) {
22            System.out.println("Error occurred: " + t);
23        }
24    }
25}

Summary Table

Key ConsiderationDescription
Runnable vs. CallableRunnable cannot throw checked exceptions, whereas Callable can.
FutureEncapsulates task execution and result retrieval.
get() BlockingRetrieves the result, blocking until the task finishes, also throws an ExecutionException if an error occurs.
Exception AggregationAllows collection and handling of exceptions from multiple tasks.
Custom HandlerImplement a custom exception handler with ThreadPoolExecutor.afterExecute.

Additional Considerations

  • Handling Checked Exceptions: The Callable interface allows for throwing checked exceptions, which can be managed in the call() method and propagated using ExecutionException.
  • Thread Interruption: Always handle the InterruptedException and restore the interrupt status when handling exceptions within the thread context.

Managing exceptions effectively in tasks submitted to an ExecutorService ensures robust multithreading applications capable of handling various runtime anomalies, thereby promoting application stability and reliability.


Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.