W3docs

Java Callable and Future

Return values from tasks with Callable and consume them asynchronously with Future — wait, timeout, cancel, propagate exceptions.

Java Callable and Future

Runnable lets a thread do work. It doesn't let the work return a value or throw a checked exception. The pair that does is Callable<V> (the producer) and Future<V> (the consumer). You submit a Callable<V> to an ExecutorService and get back a Future<V>, which is your handle for: waiting on the result, reading the value, catching the task's exception, or cancelling it.

This is the lowest-level result-aware API in Java's concurrent toolkit. The next chapter, CompletableFuture, layers on chains, combinators, and pipelines; but the contract — "an async result you can wait on" — is what Future defined first, and it's still the right tool for plain "go do this and tell me when it's done."

Callable<V> — Runnable with a return type

The interface:

@FunctionalInterface
public interface Callable<V> {
  V call() throws Exception;
}

The two differences from Runnable:

  1. Returns V (the type parameter).
  2. May throw any Exception — including checked exceptions.

Like Runnable, it's a functional interface — lambdas and method references work:

Callable<Integer> compute = () -> {
  Thread.sleep(100);
  return 42;
};

Callable<String> read = () -> Files.readString(Path.of("config.txt"));   // can throw IOException

Callable<List<Order>> query = () -> repo.findAll();                       // can throw SQLException

Callable is the right shape for any "go do this and get me a value" piece of work. Runnable is the right shape only when you genuinely don't care about a result.

Future<V> — the handle to an async result

When you submit a Callable<V>, the executor returns a Future<V>:

public interface Future<V> {
  boolean cancel(boolean mayInterruptIfRunning);
  boolean isCancelled();
  boolean isDone();
  V get() throws InterruptedException, ExecutionException;
  V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

Five methods. Three you'll use often.

get()

Blocks the calling thread until the task completes, then returns the result:

ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> f = pool.submit(() -> { Thread.sleep(100); return 42; });
Integer value = f.get();                              // blocks until done; returns 42

get() throws three things you have to handle:

  • InterruptedException — the caller was interrupted while waiting. Standard treatment: re-set the interrupt flag and propagate.
  • ExecutionException — the task itself threw something. The original exception is wrapped; access it via .getCause().
  • CancellationException — somebody called cancel() on the future.

A common shape:

try {
  Integer v = f.get();
} catch (ExecutionException e) {
  Throwable cause = e.getCause();                     // the real exception the task threw
  // ... handle cause ...
} catch (InterruptedException e) {
  Thread.currentThread().interrupt();
  // ... bail out cooperatively ...
}

get(timeout, unit)

Same as get() but with a deadline. Throws TimeoutException if the task doesn't finish in time:

try {
  Integer v = f.get(500, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
  f.cancel(true);                                     // give up; ask the task to stop
  throw new ServiceUnavailableException("timed out");
}

This is the right shape for "I'm calling out to a backend that should answer in N ms; if not, fail fast." Always pair the catch with a cancel(true) — otherwise the task continues running in the background, using a thread you no longer care about the result of.

cancel(boolean)

Asks the task to stop:

boolean cancelled = f.cancel(true);                   // true = interrupt the running thread

The argument tells the executor whether to interrupt the worker thread. With true, the worker gets an InterruptedException from any blocking call (sleep, wait, I/O); with false, the cancellation is a no-op if the task has already started — only un-started tasks are removed from the queue.

cancel is cooperative. A task that doesn't check Thread.currentThread().isInterrupted() and doesn't have any blocking calls will keep running until it finishes. Cancellation isn't a kill switch — it's a request the task has to honour.

Exceptions: the wrapping rule

Anything the Callable throws is wrapped in ExecutionException when you call get. The cause is the original throwable:

Future<Integer> f = pool.submit(() -> { throw new IOException("nope"); });
try {
  f.get();
} catch (ExecutionException e) {
  e.getCause();                                       // IOException("nope")
  e.getCause() instanceof IOException;                // true
}

Note the asymmetry: the Callable could throw a checked exception (the throws Exception in its signature), but Future.get only declares ExecutionException. The wrapping is what lets one signature carry every possible failure.

The Runnable.submit overload — pool.submit(Runnable) — returns a Future<?> whose get() returns null on success and still wraps any uncaught RuntimeException from the Runnable. That's the standard way to discover that a "fire and forget" runnable actually crashed.

Future's limits

Future is a one-way channel: you submit, you wait, you get the value. It doesn't compose:

  • You can't say "when this finishes, run that on the result."
  • You can't say "when any of these N finishes, do X."
  • You can't say "combine these two futures' results without blocking."

For all of those you need CompletableFuture (next chapter). Future is the right tool when:

  • You just want a value back from a single task.
  • You're consuming an API that returns Futures and don't need to compose.
  • The simplest contract is enough.

For modern code that does a lot of async composition, you'll mostly skip Future and reach straight for CompletableFuture — but Future is the type the executor service still returns from submit, so you'll see both.

FutureTask — the implementation behind submit

The class that powers submit. You can use it directly:

FutureTask<Integer> task = new FutureTask<>(() -> compute());
new Thread(task).start();                              // FutureTask is a Runnable
Integer v = task.get();

Most code doesn't construct FutureTask directly; the executor framework does it for you. But it's useful when you need a Future and a Runnable in one object — e.g. to schedule it on something other than an ExecutorService.

A worked example: submit, time out, propagate

The program below submits a slow task, a fast task, and a failing task; demonstrates get, get(timeout), exception unwrapping, and cancellation.

java— editable, runs on the server

What to take from the run:

  • Section 1 is the simplest shape: submit a Callable, call get, receive the value. get blocked the main thread for the 50 ms the task took. That's all Future does in its basic form — a typed, blocking handle to a result that arrives later.
  • Section 2 showed the timeout shape. The slow task would have run for 500 ms; get(100, MS) gave up after 100 and threw TimeoutException. The follow-up cancel(true) interrupted the running thread so it could exit early. Without the cancel, the task would have kept running for the remaining 400 ms — using a thread you no longer cared about the result of.
  • Section 3 showed exception wrapping. The Callable threw IOException; get() re-threw it inside ExecutionException. e.getCause() gave back the original. This is the API's universal failure channel — every checked or unchecked throw from the body lands here.
  • Section 4 showed cancellation of an unstarted task. With both pool threads busy on hog1 and hog2, the queued task was sitting in the work queue; cancel(false) removed it without ever running it. Calling get() on the cancelled future threw CancellationException — a different failure mode from "task threw" (which would have been ExecutionException).
  • Section 5 showed invokeAny. The fastest task (50 ms) won; the other two were cancelled by the executor. invokeAny is the right tool for redundant queries — call multiple sources, use the first success, abandon the rest. It's the building block behind hedged-request patterns in real systems.

What's next

The next chapter, Java CompletableFuture, introduces the composable async API — thenApply, thenCompose, allOf, anyOf, and the dozens of combinators that turn Future from a single-result handle into a full reactive pipeline.

Practice

Practice

You call `future.get()` and the task threw `SQLException` from its `call()` method. What exception does `get()` throw?