⚡ Advanced Concurrency · Advanced

Callable & Future in Java

Returning results, get() blocks, cancel, ExecutionException.

🧩 The mysteryYou send a worker off to compute something. How do you get the answer back, and what if the worker trips along the way? Meet Future: a receipt for a result that doesn't exist yet.

A task that returns

Runnable.run() returns nothing and can't throw checked exceptions. Callable<V>.call() returns a V and **may throw Exception**. Submit a lambda that returns a value and Java treats it as a Callable.

Runnable job = () -> System.out.println("hi");
Callable<Integer> task = () -> 6 * 7;

Future: a receipt

submit(callable) hands back a Future<V>: a handle to a result that may not exist yet. get() blocks until the task completes, fails, or is cancelled. It never returns null "because it isn't ready". For a non-blocking check, use isDone().

var pool = Executors.newFixedThreadPool(2);
Future<Integer> f = pool.submit(() -> 6 * 7);
doOtherWork();         // runs meanwhile
int answer = f.get();  // waits for 42
🔮 Predict it

When the task fails

The task divides by zero. What happens when main calls get()?

var pool = Executors.newFixedThreadPool(1);
try (pool) {
    Future<Integer> f =
        pool.submit(() -> 1 / 0);
    System.out.println(f.get());
}
  1. Throws ExecutionException
  2. Throws ArithmeticException
  3. Prints null
Show the answer

get() throws **ExecutionException**. The task's own ArithmeticException was stored in the Future, and get() rethrows it wrapped. The original error is the wrapper's getCause().

Unwrap the cause

Catch ExecutionException and call getCause() to get the real failure. Printing the cause here shows java.lang.ArithmeticException: / by zero, not the wrapper.

try {
    f.get();
} catch (ExecutionException e) {
    Throwable real = e.getCause();
    System.out.println(real);
}
⚠️ The trap

get() with no time limit

A request handler calls future.get() on a slow partner API. When the partner hangs, every request thread hangs with it. An unbounded get() waits forever. Use get(timeout, unit), handle TimeoutException, and fail fast.

try {
    return f.get(2, TimeUnit.SECONDS);
} catch (TimeoutException e) {
    f.cancel(true);   // try to interrupt it
    return fallback();
}
🤔 Think first

What does cancel(true) do?

You call f.cancel(true) on a task that is already running. Does it stop instantly?

Think about it, then reveal the answer

Not necessarily. cancel(true) attempts to interrupt the running thread, and the task stops only if it reacts to interruption (it's cooperative). A task that hasn't started yet simply never runs. Afterwards, get() throws CancellationException.

💼 In the real world

Fan-out pages

A product page needs price, stock and reviews from three services. Submit all three, then get() each: total latency becomes the slowest call instead of the sum of all three. Put a timeout on every get() so one slow dependency can't freeze your page.

Key takeaways

  1. Callable.call() returns V and may throw Exception
  2. get() blocks; get(timeout, unit) bounds the wait
  3. Task failure → ExecutionException; getCause() has the real error
  4. cancel(true) attempts to interrupt a running task
🤯 Did you know?

Future arrived in Java 5 with java.util.concurrent (JSR-166), led by Doug Lea and based on his earlier util.concurrent library.

Practice questions

What does this print?

try (var pool = Executors.newSingleThreadExecutor()) {
    Future<Integer> f = pool.submit(() -> 6 * 7);
    System.out.println(f.get());
}
  1. 42
  2. null
  3. 0
  4. java.util.concurrent.FutureTask@1b6d3586
Check your answer

42. The lambda returns a value, so it's a Callable<Integer>. get() waits for the result and returns 42.

What does this print?

try (var pool = Executors.newSingleThreadExecutor()) {
    Future<Integer> f = pool.submit(() -> 1 / 0);
    try {
        f.get();
    } catch (ExecutionException e) {
        System.out.println(e.getCause());
    }
}
  1. java.lang.ArithmeticException: / by zero
  2. java.util.concurrent.ExecutionException
  3. null
  4. Nothing — the exception is swallowed
Check your answer

java.lang.ArithmeticException: / by zero. The task's exception is stored in the Future. get() rethrows it wrapped in an ExecutionException, and getCause() returns the original ArithmeticException.

Next: what if you never had to block on get() at all? CompletableFuture lets you chain what happens next.