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:
- Returns
V(the type parameter). - 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 SQLExceptionCallable 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 42get() 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 calledcancel()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 threadThe 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.
What to take from the run:
- Section 1 is the simplest shape: submit a
Callable, callget, receive the value.getblocked the main thread for the 50 ms the task took. That's allFuturedoes 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 threwTimeoutException. The follow-upcancel(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
CallablethrewIOException;get()re-threw it insideExecutionException.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
hog1andhog2, thequeuedtask was sitting in the work queue;cancel(false)removed it without ever running it. Callingget()on the cancelled future threwCancellationException— a different failure mode from "task threw" (which would have beenExecutionException). - Section 5 showed
invokeAny. The fastest task (50 ms) won; the other two were cancelled by the executor.invokeAnyis 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
You call `future.get()` and the task threw `SQLException` from its `call()` method. What exception does `get()` throw?