JAVA / THREADS AND CONCURRENCY
Futures, results and handling task failure
Collect values from Callable tasks through Future.get, unwrap failures with ExecutionException.getCause, and handle timeouts and cancellation correctly.
What you will learn
- Unwrap task failures with catch (ExecutionException e) and e.getCause()
- Bound waits with get(timeout, unit); a TimeoutException does not stop the task
- Read isDone() as 'not pending' and use isCancelled() plus get() for the real outcome
- Keep every Future from submit(); an unread Future hides its exception forever
Understanding Futures, results and handling task failure
When you hand a Callable to an executor, submit returns immediately with a Future: an empty one-slot container that a worker thread will eventually fill. Success fills it with whatever call() returned; failure fills it with the Throwable that call() threw. The exception does not propagate out of submit, and it does not reach the worker thread's uncaught exception handler either, because the FutureTask wrapping your task catches everything and records it as the outcome. Crossing a thread boundary converts a thrown exception into stored data.
get() is where that stored outcome becomes an event in your own thread. It blocks until the slot is filled, then either returns the value or throws ExecutionException, with the task's original throwable available from getCause(). The wrapping is not bureaucracy: the stack that failed has already unwound on another thread, so the exception you catch carries your call site's trace while getCause() preserves the task's trace. It also gives get() one checked exception type to declare, even though Callable.call is allowed to throw any Exception.
Finished is not the same as succeeded. A Future reaches one of three terminal states - value, exception, or cancelled - and isDone() returns true for all three, so it only answers the question 'is it still pending?'. isCancelled() separates the third case and only get() separates the first two. The practical consequence is that a Future nobody calls get() on is a failure nobody ever sees: the exception simply sits in the object until it is collected.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class TaskResults {
static Callable<Integer> trimmedLength(String text) {
return () -> text.trim().length();
}
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> good = pool.submit(trimmedLength(" concurrency "));
Future<Integer> bad = pool.submit(trimmedLength(null));
System.out.println("both submitted, nothing has thrown yet");
try {
System.out.println("good = " + good.get());
} catch (ExecutionException e) {
System.out.println("good failed: " + e.getCause());
}
try {
System.out.println("bad = " + bad.get());
} catch (ExecutionException e) {
Throwable cause = e.getCause();
System.out.println("bad failed with " + cause.getClass().getName());
System.out.println("wrapper was " + e.getClass().getSimpleName());
}
System.out.println("bad.isDone() = " + bad.isDone());
pool.shutdown();
}
}A Future holds either the task's value or the Throwable it died with, and get() is the only place that outcome enters your thread - as a return value, or as an ExecutionException whose cause is the original exception.
Worked examples
Timeout, then cancel
Shows that a TimeoutException on get() leaves the task running, and that cancelling changes which exception get() throws afterwards.
import java.util.concurrent.*;
public class SlowTask {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newSingleThreadExecutor();
Callable<String> slow = () -> {
Thread.sleep(2000);
return "done";
};
Future<String> f = pool.submit(slow);
try {
f.get(200, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
System.out.println("gave up waiting; still running = " + !f.isDone());
}
System.out.println("cancel(true) returned " + f.cancel(true));
System.out.println("isCancelled = " + f.isCancelled() + ", isDone = " + f.isDone());
try {
f.get();
} catch (CancellationException e) {
System.out.println("get() now throws " + e.getClass().getSimpleName());
}
pool.shutdown();
}
}Example explained
Line 1get(200, TimeUnit.MILLISECONDS) throws TimeoutException: the timeout limits how long the caller waits, not how long the task runs, which is why isDone() is still false.
Line 2cancel(true) returns true because the task had not completed yet; the true argument asks the pool to interrupt the thread running it, which ends the sleep.
Line 3isDone() is true after cancellation even though no value was ever produced - done means 'no longer pending'.
Line 4A later get() throws CancellationException, which is unchecked and is not an ExecutionException, so it needs its own catch clause.
One bad task in a batch
Uses invokeAll to run three parsing tasks and reads each Future separately so a single failure does not lose the other results.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class BatchResults {
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(3);
List<Callable<Integer>> jobs = new ArrayList<>();
for (String raw : List.of("10", "oops", "32")) {
jobs.add(() -> Integer.parseInt(raw));
}
List<Future<Integer>> results = pool.invokeAll(jobs);
int total = 0;
for (int i = 0; i < results.size(); i++) {
try {
total += results.get(i).get();
} catch (ExecutionException e) {
System.out.println("job " + i + " failed: " + e.getCause().getMessage());
}
}
System.out.println("total = " + total);
pool.shutdown();
}
}Example explained
Line 1invokeAll blocks until every task has finished, so each Future in the returned list is already done - but done covers failed as well as succeeded.
Line 2The returned list keeps the order of the task collection, which is how index 1 identifies the "oops" job.
Line 3e.getCause() hands back the real NumberFormatException, so you can branch on its type instead of parsing message text.
Line 4The failure is confined to one Future: jobs 0 and 2 still contribute 10 and 32 to the total.
Important notes
cancel(true) only interrupts the worker thread. A task that never blocks and never checks Thread.currentThread().isInterrupted() runs to completion even though isCancelled() already reports true, and cancel() on an already-finished task returns false and changes nothing.
InterruptedException from get() means your waiting thread was interrupted, not that the task failed; the task keeps running and its outcome stays in the Future.
Common mistakes
Calling submit(task) and discarding the returned Future: a task that throws leaves no trace at all, so the work silently did nothing and the missing data shows up much later somewhere unrelated.
Trying to catch the task's own exception type around get(), for example catch (NumberFormatException e): it never matches because the throwable arrives wrapped in ExecutionException, and with a checked task exception such as IOException the compiler rejects the catch outright.
Calling get() inside the submission loop: each iteration blocks until that task finishes, so the pool runs one task at a time and the batch takes as long as the sequential version.
Try it yourself
Change, predict, then run
Submit three Callable<Integer> tasks that compute 100 / 5, 100 / 0 and 100 / 2, and store the three Futures in a list. Loop over the list and print either the quotient or the simple class name of e.getCause(), and confirm the two good results still print after the failing one.
Open the Java workspaceCheck your understanding
You submit a task with submit(), the task throws IllegalArgumentException, and your code never calls get() on the returned Future. What happens?
- The worker thread prints the stack trace through the default uncaught exception handler
- The exception is rethrown on the main thread at the next submit() call
- The exception is stored as the Future's outcome, and since nobody calls get() it is never seen
- The ExecutorService shuts itself down and rejects any later tasks
Show answer
submit wraps your task in a FutureTask whose run() catches every Throwable and records it as the completion value, so the exception becomes stored state rather than an escaping error. The first option is what execute(Runnable) would give you: there nothing captures the throwable, it escapes run(), and the thread's uncaught exception handler prints it.