In Java, Callable and Future are the two interfaces behind a background task that returns a result. The Callable is the task, which returns a value and may throw a checked exception, and the Future is the handle that ExecutorService.submit() returns, which we use to wait for the result, check the task’s state or cancel it. Both interfaces live in java.util.concurrent and have been part of Java since version 5.
We use a Callable and Future pair whenever work should run in the background while the current thread does something else, such as totaling invoices, calling a remote service or parsing a large file, and the current thread needs the answer later.
The following example submits a word-count task and reads its result through the Future.
Callable<Integer> countWords = () -> "the quick brown fox".split(" ").length;
Future<Integer> future;
try (ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) {
future = pool.submit(countWords); // returns at once, the task runs in the pool
// the current thread can do other work here
}
Integer words = future.get(); // 4
Future.State state = future.state(); // SUCCESS
boolean done = future.isDone(); // true
Notice that submit() returns without waiting for the task, and get() is the call that delivers the value. We compare Callable with Runnable, go through every method of Future, handle timeouts, exceptions and cancellation, use the Java 19 methods state() and resultNow(), and finish with FutureTask and CompletableFuture.
1. Callable vs Runnable
The Callable interface has one method, V call() throws Exception, which returns a value of the type parameter V and is allowed to throw any checked exception. It is a functional interface, so a lambda such as () -> 42 or a method reference can implement it.
A Runnable has a run() method that returns nothing and cannot throw checked exceptions. That difference decides which one we pick, and the Runnable vs Callable article covers it in more depth.
| Feature | Runnable | Callable<V> |
|---|---|---|
| Method | void run() | V call() throws Exception |
| Returns a value | No | Yes |
| Checked exceptions | Must be caught inside run() | Can be thrown, and Future.get() reports them |
| Runs on a plain Thread | Yes, new Thread(runnable) | Only through a FutureTask |
| Submitted to an executor | execute() or submit() | submit(), invokeAll(), invokeAny() |
When a task has no result but needs to throw a checked exception, we declare it as Callable<Void> and return null.
2. The Future Interface and Its States
A Future represents the result of a computation that may still be running. The task moves from running to one final state, and every method of Future either waits for that state or reports it.

The methods split into blocking calls and calls that return at once.
| Method | Blocks | Returns or throws |
|---|---|---|
| get() | Yes, until the task is done | The result, or ExecutionException, CancellationException, InterruptedException |
| get(timeout, unit) | Up to the timeout | Same as get(), plus TimeoutException |
| isDone() | No | true once the task succeeded, failed or was cancelled |
| cancel(mayInterrupt) | No | false if the task had already completed |
| isCancelled() | No | true if the task was cancelled before it completed |
| state() (Java 19) | No | RUNNING, SUCCESS, FAILED or CANCELLED |
| resultNow() (Java 19) | No | The result, or IllegalStateException if the task did not succeed |
| exceptionNow() (Java 19) | No | The exception, or IllegalStateException if the task did not fail |
Notice that isDone() returns true for all three final states, so it tells us only that waiting is over, not that a value exists. The state() method answers both questions in one call.
3. Callable and Future Example
An accounting app that prints a monthly statement for each customer is a good use case. Totaling a customer’s invoices means reading them from a database, so the app starts one task per customer and collects the totals afterwards. The following example uses a record for the invoice amounts and a method that builds the task.
record Invoices(String customer, List<Integer> amounts) {}
static void pause(long ms) {
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(ms));
}
static Callable<Integer> totalOf(Invoices invoices, long loadMs) {
return () -> {
pause(loadMs); // simulates the database call
return invoices.amounts().stream().mapToInt(Integer::intValue).sum();
};
}
Each call to submit() returns a Future<Integer> at once, and the two tasks run in parallel. The get() calls come last, so the current thread waits only for whichever task is still running.
Invoices anna = new Invoices("anna", List.of(120, 80, 50));
Invoices ben = new Invoices("ben", List.of(300, 25));
Future<Integer> annaTotal;
Future<Integer> benTotal;
try (ExecutorService pool = Executors.newFixedThreadPool(2)) {
annaTotal = pool.submit(totalOf(anna, 100));
benTotal = pool.submit(totalOf(ben, 50));
}
int a = annaTotal.get(); // 250
int b = benTotal.get(); // 325
int sum = a + b; // 575
Since Java 19, the try-with-resources block closes the executor and waits for both tasks, so no thread is left running after the block ends. Before Java 19, we called shutdown() in a finally block, which the executor shutdown article explains.
3.1. Calling get() with a Timeout
A plain get() waits forever, which turns a hung database call into a hung request. The timed get() throws a TimeoutException instead, and the safe version cancels the task so it stops using a pool thread.
static int totalOrZero(Future<Integer> future, long timeoutMs) throws InterruptedException {
try {
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
future.cancel(true); // stop the slow task
return 0;
} catch (ExecutionException e) {
return 0; // the task threw, see section 4
}
}
Invoices anna = new Invoices("anna", List.of(120, 80, 50));
Invoices ben = new Invoices("ben", List.of(300, 25));
int onTime;
int late;
try (ExecutorService pool = Executors.newFixedThreadPool(2)) {
onTime = totalOrZero(pool.submit(totalOf(anna, 20)), 500);
late = totalOrZero(pool.submit(totalOf(ben, 2000)), 50);
}
int fast = onTime; // 250
int slow = late; // 0, cancelled after 50 ms
The timeout applies to the waiting thread, not to the task. Without the cancel(true) call, the slow task would keep running, and the close() at the end of the try block would wait the full two seconds for it.
4. Handling Exceptions Thrown by call()
An exception thrown inside call() never reaches the thread that submitted the task until that thread asks the Future. The get() method wraps it in an ExecutionException, and getCause() returns the original exception.
Callable<Integer> broken = () -> Integer.parseInt("12x");
Future<Integer> failed;
try (ExecutorService pool = Executors.newSingleThreadExecutor()) {
failed = pool.submit(broken);
}
Future.State outcome = failed.state(); // FAILED
String cause = failed.exceptionNow().getClass().getSimpleName(); // "NumberFormatException"
Integer value = failed.get(); // ExecutionException: NumberFormatException
A common bug follows from that rule. If we submit a task and never call get(), its exception disappears without a log line, because the executor stores it in the Future instead of passing it to the thread’s UncaughtExceptionHandler. Code that does not need a result either calls get() anyway or catches and logs exceptions inside the task.
5. Cancelling a Task
The cancel(boolean mayInterruptIfRunning) method stops a task that has not started yet, and with true it also interrupts the thread of a task that is already running. After a successful cancel, get() throws a CancellationException.
A user who closes the statement screen before the totals are ready is the typical case. The app cancels the futures so the database calls stop.
Invoices anna = new Invoices("anna", List.of(120, 80, 50));
Future<Integer> statement;
boolean stopped;
try (ExecutorService pool = Executors.newSingleThreadExecutor()) {
statement = pool.submit(totalOf(anna, 5000));
stopped = statement.cancel(true);
}
boolean result = stopped; // true
boolean isCancelled = statement.isCancelled(); // true
Future.State after = statement.state(); // CANCELLED
Integer total = statement.get(); // CancellationException
Interrupting only works when the task reacts to the interrupt, for example by calling a blocking method that throws InterruptedException or by checking Thread.currentThread().isInterrupted() in a loop. A task stuck in a tight loop that never checks the flag keeps running even though its Future already reports CANCELLED. The cancel a task guide covers more cases.
6. Checking a Future Without Blocking (Java 19)
Before Java 19, reading a finished Future still meant calling get() and catching two checked exceptions. The state(), resultNow() and exceptionNow() methods make it possible to read a completed task in a switch or a stream.
static String describe(Future<Integer> future) {
return switch (future.state()) {
case RUNNING -> "still loading";
case SUCCESS -> "total " + future.resultNow();
case FAILED -> "error: " + future.exceptionNow().getMessage();
case CANCELLED -> "cancelled";
};
}
Future<Integer> ok = CompletableFuture.completedFuture(250);
Future<Integer> bad = CompletableFuture.failedFuture(new IllegalStateException("db down"));
String first = describe(ok); // "total 250"
String second = describe(bad); // "error: db down"
Integer early = new CompletableFuture<Integer>().resultNow(); // IllegalStateException
We can see that resultNow() throws an IllegalStateException when the task is not finished, so we call it only after checking state() or after a method such as invokeAll() that returns completed futures.
7. Running a Callable with FutureTask
The FutureTask class implements both Runnable and Future. It wraps a Callable, so we can run it on a plain Thread and still read the result through the Future methods. Executors use FutureTask internally when we call submit().
Invoices ben = new Invoices("ben", List.of(300, 25));
FutureTask<Integer> task = new FutureTask<>(totalOf(ben, 10));
Thread worker = Thread.ofVirtual().start(task);
int benSum = task.get(); // 325
Creating threads by hand is rarely needed in application code, so FutureTask shows up mostly in libraries and tests. In application code, an executor manages the threads for us.
8. Future vs CompletableFuture
A Future can only be polled or waited on, so combining two results means blocking on both. The CompletableFuture class implements Future and adds callbacks, chaining and combining without blocking.
| Need | Future from submit() | CompletableFuture |
|---|---|---|
| Get the result | get(), blocks | join() or get(), or a callback |
| Run code when the result arrives | Not possible without a blocking thread | thenApply(), thenAccept() |
| Combine two results | Call get() on both | thenCombine(), allOf() |
| Handle an exception | Catch ExecutionException | exceptionally(), handle() |
| Complete it by hand | No | complete(), completeExceptionally() |
For one background task with one result, submit() and get() are enough and easier to read. Once results feed into each other, CompletableFuture is the better choice, and the Java concurrency guide shows where it fits next to the other tools.
9. Callable and Future FAQs
A blocking get() call and a task that ignores cancel() are behind most of the questions about these two interfaces.
9.1. Can we run a Callable with a Thread?
No, because the Thread constructor accepts a Runnable. We wrap the Callable in a FutureTask, pass that to the thread, and read the result with get(), as shown in section 7.
9.2. Is Future.get() a blocking call?
Yes. It blocks until the task completes, so we either call it at the point where we need the value, use the timed version, or check isDone() and state() first. For a non-blocking style, we use CompletableFuture callbacks.
9.3. What does get() return for a Runnable submitted with submit()?
The get() method returns null once the task completes normally, because a Runnable has no result. The overload submit(Runnable, T result) returns the given value instead.
9.4. Can we call get() more than once?
Yes. Once the task is done, every call returns the same result or throws the same exception at once, without running the task again.
9.5. Does cancel(true) stop a running task?
Only if the task reacts to the interrupt. The cancel() call marks the Future as cancelled and interrupts the worker thread, and the task must check the interrupt flag or call a blocking method that throws InterruptedException.
10. Conclusion
A Callable is a task with a result and checked exceptions, and the Future returned by submit() is how we read that result later. The get() method blocks and reports failures as ExecutionException or CancellationException, so production code uses the timed version and cancels the task on timeout.
Since Java 19, state(), resultNow() and exceptionNow() read finished tasks without checked exceptions, and try-with-resources closes the executor. When results need to be chained or combined, we move on to CompletableFuture.
11. References
- Callable Javadoc (Java 25)
- Future Javadoc (Java 25)
- FutureTask Javadoc (Java 25)
- ExecutorService Javadoc (Java 25)
Happy Learning !!
I just saw in Java 8 ,The Future‘s method get(longtimeout,TimeUnitunit)shouldt hrows TimeoutException if the wait timed out, not return null .
Yep, you are right it should throw the TimeoutException and not null. Here is the implementation.
public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { if (unit == null) throw new NullPointerException(); int s = state; if (s <= COMPLETING && (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING) throw new TimeoutException(); return report(s); }how to do read file content and write same content into another file using two callable thread?
Good One
Thank you for the great article.
But, how to run task periodically (every n seconds) and retrieve some result, which is needed in further processing? Is it wise to use in indefinite while(true) loop, the method schedule (Callable task, long delay, TimeUnit timeunit)?
Here is the code
class TestTRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
System.out.println(“Rejected Execution Handler For Thread ” + Thread.currentThread().getName());
Thread.sleep(1000);
} catch (Exception e2) {
// TODO: handle exception
}
executor.execute(r);
}
}
class TestTTask implements Callable {
@Override
public String call() {
try {
Thread.sleep(1000);
} catch (Exception e) {
// TODO: handle exception
}
return Thread.currentThread().getName();
}
}
public class TestT {
public static void main(String[] args) throws InterruptedException, ExecutionException {
BlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(
1);
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 2,
1000, TimeUnit.MILLISECONDS, arrayBlockingQueue,
new TestTRejectedExecutionHandler());
for (int iLoop = 0; iLoop < 100; iLoop++) {
Future future = threadPoolExecutor.submit(new TestTTask());
System.out.println(future.get());
}
}
}
Here is my Example.
In this If i call future.get(), then rejectedExecution doesn’t get executed, but if comment that line(future.get()) then it is getting executed.
Please Suggest a way forward.
Hi Dushyant,
get() is blocking call so calling that method after submitting will not give better result since you are waiting the just submitted task. Replace the loop with the below code instead.
List<Future<String>> list = new ArrayList<>(); for (int iLoop = 0; iLoop < 10; iLoop++) { Future<String> future = threadPoolExecutor.submit(new TestTTask()); list.add(future); } //getting the results of all submitted task for(Future<String> future:list){ System.out.println("Thread Name: "+future.get()); }Hello
How would you handle Rejected Task if i use ArrayBlockingQueue and choose to implement my own RejectedExecutionHandler Class.
Dushyant Sapra
Very nice article about Future, Callable and Executor. I wandered here and there for basic information and use, but didn’t find as easiest as this. Thanks Lokesh. Thanks a lot.
very very useful information, to know any new subject I will first search in your site. if I didnot find then I go for google search.
Thanks Venkat. I am glad that my work is of some use !!
One of the easiet article to unerstand ThreadPool in details.
Thanks for nice article sir.