Back to Blog
Java

Java Callable: Return Results from Concurrent Tasks

java callable: Learn how to use Java Callable to return results from concurrent tasks, handle exceptions, and coordinate work with ExecutorService and Future.

java concurrencyexecutor servicefuturecallable interfacemultithreading
Diagram showing a Java Callable task submitted to an executor returning a Future result

Java Callable, defined as java.util.concurrent.Callable, is the standard way to represent a task that produces a result and can throw a checked exception. Unlike Runnable, whose run() method returns void and cannot throw checked exceptions, Callable declares a single method call() that returns a value of a specified type. That difference changes how you structure concurrent code: instead of writing results into a shared mutable field, you can submit a Callable to an ExecutorService and receive the result through a Future.

What Callable Adds Beyond Runnable

Runnable has existed since Java 1.0 and is fine for fire-and-forget work. The interface is minimal:

public interface Runnable { void run(); }

A Runnable cannot return a value, and any checked exception must be caught inside run(). That leads to awkward patterns: storing the result in a field, catching exceptions manually, and coordinating completion with latches or other synchronization primitives.

Callable was introduced in Java 5 as part of java.util.concurrent:

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

The generic type parameter V is the result type. The call() method can throw any checked exception, which the executor framework propagates back to the caller when the Future is resolved. Because Callable is a functional interface, you can write it as a lambda in Java 8 and later.

Declaring and Submitting a Callable

The most direct use of a Callable is submitting it to an ExecutorService. The executor runs the task on a worker thread and returns a Future that represents the pending result.

ExecutorService executor = Executors.newFixedThreadPool(4); Callable<Integer> computeSum = () -> { int sum = 0; for (int i = 1; i <= 1000; i++) { sum += i; } return sum; }; Future<Integer> future = executor.submit(computeSum);

The lambda returns an Integer, which matches the generic type of both the Callable and the Future. The submit method accepts the Callable, schedules it, and returns immediately. The actual computation runs on a thread from the pool.

Reading Results with Future

The Future object is the handle through which you retrieve the result and control the task's lifecycle. Its most important methods are get() and get(long, TimeUnit).

try { Integer result = future.get(); System.out.println("Sum: " + result); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { Throwable cause = e.getCause(); // The original exception thrown by call() }

get() blocks until the task completes. If the task threw an exception, get() wraps it in an ExecutionException, and the original exception is available through getCause(). If the calling thread was interrupted while waiting, get() throws InterruptedException; the interrupted flag should be restored with Thread.currentThread().interrupt().

A timed get() prevents waiting forever:

try { Integer result = future.get(5, TimeUnit.SECONDS); } catch (TimeoutException e) { future.cancel(true); }

When a timeout occurs, the task is still running on the worker thread. Calling cancel(true) requests interruption of that thread. Whether the task actually stops depends on whether it checks the interrupt flag inside its loop.

Callable vs Runnable: When to Use Which

AspectCallable<V>Runnable
Return valueYes, generic type VNo, void
Checked exceptionsCan throw any ExceptionMust be handled inside run()
Functional interfaceYesYes
Used withExecutorService, FutureExecutorService, Thread
Typical useTasks that produce a resultFire-and-forget tasks

Use Runnable when the task has no result to return and no checked exception to propagate, such as logging, flushing a cache, or sending a notification. Use Callable whenever the task computes a value, fetches data, or performs an operation whose failure should be visible to the caller.

A common mistake is wrapping a Callable in a Runnable just to fit an existing API. If you control the executor, prefer submitting the Callable directly so the result and exception flow through the Future instead of being stored in a shared field.

Handling Exceptions and Timeouts

Because call() can throw any checked exception, the executor framework treats the exception as part of the task's outcome. The exception is captured when the task fails and is rethrown wrapped in ExecutionException when you call get(). This is more reliable than catching exceptions inside a Runnable, where an uncaught exception would propagate to the thread's uncaught exception handler and be invisible to the submitting code.

Consider a task that performs a remote lookup:

Callable<Price> fetchPrice = () -> { if (!service.isAvailable()) { throw new ServiceUnavailableException("price service is down"); } return service.getPrice("AAPL"); }; Future<Price> future = executor.submit(fetchPrice); try { Price price = future.get(); } catch (ExecutionException e) { if (e.getCause() instanceof ServiceUnavailableException) { // fall back to a cached price } }

The caller can distinguish the task's failure from an infrastructure failure because the original exception type is preserved. This makes Callable well suited to batch jobs where individual tasks fail independently and should not abort the whole batch.

Running Multiple Callables in Parallel

When you have several independent tasks, you can submit them all and wait for completion. ExecutorService provides invokeAll() and invokeAny() for this purpose.

List<Callable<Document>> tasks = new ArrayList<>(); tasks.add(() -> loadDocument("doc1.txt")); tasks.add(() -> loadDocument("doc2.txt")); tasks.add(() -> loadDocument("doc3.txt")); List<Future<Document>> futures = executor.invokeAll(tasks); for (Future<Document> future : futures) { Document doc = future.get(); process(doc); }

invokeAll() submits all tasks and returns a list of Future objects in the same order as the input list. It blocks until all tasks finish. invokeAny() returns the result of the first task that completes successfully, cancelling the remaining tasks; it is useful when any one valid result is enough, such as querying multiple replicas.

Concurrency Considerations and Thread Pool Fit

The thread pool size directly affects how many Callable tasks run concurrently. A pool sized to the number of CPU cores is appropriate for CPU-bound tasks. For I/O-bound tasks, a larger pool avoids blocking all workers on network or disk waits. If you submit more tasks than the pool can run, the excess waits in the executor's queue.

The Future returned by submit() does not automatically cancel the task when the caller stops waiting. If you abandon a Future without calling get() or cancel(), the task continues running to completion on the worker thread. That can be a subtle source of resource leaks in long-running applications. Always decide whether the task should be cancelled on timeout or shutdown, and call cancel(true) when appropriate.

The ExecutorService should be shut down explicitly when the application no longer needs it. Calling shutdown() stops accepting new tasks and lets running tasks finish; shutdownNow() attempts to interrupt running tasks. Without a shutdown, worker threads remain alive and prevent the JVM from exiting.

java callable: Practical Usage and Code Examples | RYUSLOG DEV