Skip to content
Java multithreading 5 min read

Callable & Future

When you need a background task to return a value or throw a checked exception, Runnable falls short. That’s exactly why Java introduced Callable<V> and Future<V> — a clean pair of interfaces that give your threads a voice to report back results (or errors) to the caller.

What Are Callable and Future?

InterfacePackageKey MethodReturnsCan throw?
Runnablejava.langrun()voidNo checked exceptions
Callable<V>java.util.concurrentcall()VYes — any checked exception
Future<V>java.util.concurrentget()VWraps exception in ExecutionException

Think of Callable as a smarter Runnable, and Future as a receipt you hold while the task runs elsewhere — you can redeem it later for the result.

Callable in Action

Callable<V> is a functional interface with one method: V call() throws Exception.

import java.util.concurrent.Callable;

public class SumCallable implements Callable<Integer> {
    private final int limit;

    public SumCallable(int limit) {
        this.limit = limit;
    }

    @Override
    public Integer call() {
        int sum = 0;
        for (int i = 1; i <= limit; i++) {
            sum += i;
        }
        return sum;
    }
}

You can’t run a Callable with new Thread() — you submit it to an ExecutorService.

Submitting to an ExecutorService

import java.util.concurrent.*;

public class CallableDemo {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        Callable<Integer> task = new SumCallable(100);
        Future<Integer> future = executor.submit(task);  // returns immediately

        System.out.println("Task submitted. Doing other work...");

        int result = future.get();  // blocks until result is ready
        System.out.println("Sum = " + result);

        executor.shutdown();
    }
}

Output:

Task submitted. Doing other work...
Sum = 5050

executor.submit() returns a Future<V> right away. Your main thread continues until it actually needs the result — at which point future.get() blocks if the task isn’t done yet.

Using Lambdas with Callable

Because Callable is a functional interface, you can use a lambda expression to keep things concise:

import java.util.concurrent.*;

public class LambdaCallable {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        Future<String> future = executor.submit(() -> {
            Thread.sleep(500);  // simulate work
            return "Hello from a Callable!";
        });

        System.out.println("Waiting for result...");
        System.out.println(future.get());

        executor.shutdown();
    }
}

Output:

Waiting for result...
Hello from a Callable!

Future Methods

Future<V> gives you fine-grained control over the task:

MethodDescription
get()Blocks indefinitely until result is available
get(long timeout, TimeUnit unit)Blocks for at most the given time
isDone()Returns true if task completed (normally, cancelled, or with exception)
isCancelled()Returns true if task was cancelled before completing
cancel(boolean mayInterruptIfRunning)Attempts to cancel the task

get() with a Timeout

Waiting forever can freeze your application. Always prefer get(timeout, unit) in production:

import java.util.concurrent.*;

public class FutureTimeout {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        Future<String> future = executor.submit(() -> {
            Thread.sleep(3000);  // takes 3 seconds
            return "Slow result";
        });

        try {
            String result = future.get(1, TimeUnit.SECONDS);  // only wait 1s
            System.out.println(result);
        } catch (TimeoutException e) {
            System.out.println("Task took too long! Cancelling...");
            future.cancel(true);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            executor.shutdown();
        }
    }
}

Output:

Task took too long! Cancelling...

Warning: If your Callable throws an exception, future.get() wraps it in an ExecutionException. Always call e.getCause() to get the original exception.

Handling Exceptions from Callable

import java.util.concurrent.*;

public class CallableException {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        Future<Integer> future = executor.submit(() -> {
            if (true) throw new ArithmeticException("Division by zero!");
            return 42;
        });

        try {
            int result = future.get();
        } catch (ExecutionException e) {
            System.out.println("Task failed: " + e.getCause().getMessage());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            executor.shutdown();
        }
    }
}

Output:

Task failed: Division by zero!

Running Multiple Callables

When you have many tasks, invokeAll() submits them all and returns a list of Futures once all complete. invokeAny() returns the result of the first successful task.

import java.util.concurrent.*;
import java.util.*;

public class MultipleCallables {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        List<Callable<String>> tasks = List.of(
            () -> "Result from task 1",
            () -> "Result from task 2",
            () -> "Result from task 3"
        );

        List<Future<String>> futures = executor.invokeAll(tasks);

        for (Future<String> f : futures) {
            System.out.println(f.get());
        }

        executor.shutdown();
    }
}

Output:

Result from task 1
Result from task 2
Result from task 3

Tip: Use invokeAny() for race-condition patterns — for example, querying multiple services and taking whoever responds first.

CompletableFuture: The Modern Alternative

Java 8 introduced CompletableFuture<T>, which extends Future with non-blocking callbacks, chaining, and combining. It’s the preferred choice for new asynchronous code:

import java.util.concurrent.CompletableFuture;

public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception {
        CompletableFuture<String> future = CompletableFuture
            .supplyAsync(() -> "Hello")          // runs async
            .thenApply(s -> s + ", World!")      // transforms result
            .thenApply(String::toUpperCase);     // chains another step

        System.out.println(future.get());
    }
}

Output:

HELLO, WORLD!

Note: CompletableFuture doesn’t require an ExecutorService directly — it defaults to the common ForkJoinPool. For production workloads, pass your own executor to supplyAsync(task, executor).

Under the Hood

When you call executor.submit(callable), the ExecutorService wraps your Callable inside a FutureTask<V> — which implements both Runnable (so it can be handed to a worker thread) and Future<V> (so you can query the result).

Internally, FutureTask uses a state machine with states like NEW, COMPLETING, NORMAL, EXCEPTIONAL, and CANCELLED. The result is stored in a field protected by volatile semantics and a happens-before relationship (see Java Memory Model), so a thread calling get() safely sees the value written by the worker thread.

get() blocks using LockSupport.park(), which suspends the calling thread without spinning. When the task finishes, the worker calls LockSupport.unpark() on all waiting threads — making it CPU-efficient while you wait.

For virtual threads (Project Loom, Java 21), Callable/Future work exactly the same way — you simply submit to a virtual-thread executor (Executors.newVirtualThreadPerTaskExecutor()), and blocking on future.get() no longer blocks an OS thread.

Quick Comparison

FeatureRunnableCallable<V>
Return valueNoYes (V)
Checked exceptionsNoYes
Can be used with ThreadYesNo (needs ExecutorService)
Can be used as lambdaYesYes
Available sinceJava 1.0Java 5
Last updated June 13, 2026
Was this helpful?