Java Callable and Future

In the last lesson you learned about Java ExecutorService and thread pool types. Those tasks were Runnable, which returns nothing. But often you want a background task to give back a result, and for that Java has Callable and Future.

πŸ€” The limitation of Runnable

Picture this. You ask a helper thread to add up a long list of numbers while your main thread does other things. The helper does the math. Then you reach for the answer and there is no door to hand it through. That is the wall you hit with Runnable.

A Runnable does work but cannot return anything. Its run method returns void, meaning β€œno value at all.” So if a background task computes a number, there is no clean way to send it back.

This Runnable computes a value but cannot pass it back out.

Runnable task = () -> {
int result = 2 + 2; // ❌ computed, but how do we return it?
// run() returns void, so the result is stuck inside the task
};

Let’s name what is going wrong here.

  • The value lives inside the lambda, on the helper thread.
  • run returns void, so there is no return slot to put it in.
  • The main thread has no handle to reach in and grab it.

People sometimes work around this with a shared variable, but that path is full of timing and lock bugs just to read one number. We need a task that returns a value and a way to receive it later. That is what Callable and Future provide.

Runnable cannot do one more thing. Its run method cannot throw a checked exception, an error type Java forces you to declare or catch. So it cannot pass a failure back either. Callable solves both problems at once.

🧩 Callable: a task that returns a value

A Callable is like a Runnable, but its method is named call, and call returns a result. It is also a functional interface, so you can write it as a lambda. The difference: call gives back a value and can throw a checked exception.

This Callable adds the numbers 1 through 100 and returns the total as an Integer.

import java.util.concurrent.Callable;
// βœ… a Callable that returns an Integer
Callable<Integer> task = () -> {
int sum = 0;
for (int i = 1; i <= 100; i++) sum += i;
return sum; // βœ… Callable CAN return a value
};

Let’s read this line by line.

  • Callable<Integer> means β€œa task that produces an Integer when it runs”.
  • The lambda body loops and builds up a sum.
  • The return sum; hands the answer back, which Runnable could never do.

The type inside the angle brackets is whatever the task produces. Text means Callable<String>. A list of orders means Callable<List<Order>>. The shape flexes to fit your work.

But the task runs on another thread, possibly long after you started it. So when call returns its value, where does it go? You need somewhere to hold it until you read it. That holding place is a Future.

🧩 Future: a promise of a result

A Future represents a result that is not ready yet but will be. Submitting a Callable gives you a Future immediately. The task runs in the background, and later you ask the Future for the result.

Think of ordering food at a counter. You get a token. The food is not ready, but the token stands in for your order. When it is done, you use the token to collect it. A Future is that token. What it gives you:

  • It is handed back the instant you submit, so your thread is never blocked at submit time.
  • It holds the result the moment the background task finishes.
  • It lets you decide when to collect that result.

So Callable is the worker that produces a value, and Future is the claim ticket. Now let’s put them together.

πŸ› οΈ Putting Callable and Future together

The flow is always the same. You submit a Callable to an ExecutorService, get a Future back, and call get() to fetch the result when you need it.

This program submits one task, prints a message while it runs, then collects the result.

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(2);
Callable<Integer> task = () -> {
int sum = 0;
for (int i = 1; i <= 100; i++) sum += i;
return sum;
};
Future<Integer> future = pool.submit(task); // βœ… returns immediately
System.out.println("Task submitted, doing other work...");
Integer result = future.get(); // waits here until the result is ready
System.out.println("Result: " + result);
pool.shutdown();
}
}

Let’s walk through what each step is doing.

  • pool.submit(task) hands the task to the pool and instantly returns a Future, without waiting for the math to finish.
  • The next line runs straight away, so the main thread is free to do real work in the meantime.
  • future.get() is the moment of truth. It waits until the result is ready, then returns it.
  • The sum of 1 to 100 is 5050, and that value comes back through the Future.
  • pool.shutdown() tells the pool no more tasks are coming, so the program can end cleanly.

Notice main declares throws Exception, because get() can throw. We cover that soon. For now, just know you must deal with it.

Output

Task submitted, doing other work...
Result: 5050

The order tells the whole story. The β€œdoing other work” message prints before the result, because submit did not block. That gap is where your parallelism lives.

⏳ How get() behaves

The key thing about get() is one word: it blocks. The calling thread stops and waits, doing nothing else, until the result is ready.

This snippet shows the pause point clearly.

Future<Integer> future = pool.submit(task);
// ... main thread is free to do other things right here ...
Integer result = future.get(); // βœ… blocks until the result is available

Here is how get() decides what to do.

  • If the task is already finished, get() returns the result instantly, with no wait.
  • If the task is still running, the calling thread pauses until the task finishes.
  • Once the result arrives, get() hands it back and your code carries on.

So the timing of your get() call matters. Do your other work first, then call get(), so the background task and your main thread overlap. That overlap is the whole point.

If you do not want to wait at all, the Future has other tools:

  • future.isDone() returns true or false right away, telling you whether the result is ready, without blocking.
  • future.get(timeout, unit) waits, but only up to a limit. If the result is not ready in time it throws a TimeoutException, so a slow task cannot freeze you forever.
  • future.cancel(true) asks the executor to stop the task if it has not finished. It returns true if the cancel request succeeded.
  • future.isCancelled() tells you whether the task was cancelled before it completed.

This loop keeps the main thread busy and only collects the result once the task is done.

Future<Integer> future = pool.submit(task);
while (!future.isDone()) {
System.out.println("Still working... doing other useful stuff");
// βœ… do real work here instead of just waiting
}
Integer result = future.get(); // result is ready, so this returns instantly
System.out.println("Result: " + result);

In real programs you would do meaningful work inside that loop. The point is that isDone() lets you check in without freezing.

get() waits, so call it at the right time

future.get() blocks the calling thread until the result is ready. If you call it right after submitting, you lose the parallelism, because you just sit and wait for the task to finish. Do your other work first, then call get() when you actually need the result.

πŸ› οΈ Submitting several tasks and collecting results

A thread pool shines when you run many tasks at once. Submit several Callables, keep all the Futures, then collect every result. The tasks run in parallel, so the total time is closer to the slowest single task than the sum of all of them.

This program submits three squaring tasks, then loops over the Futures to gather every answer.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(3);
List<Future<Integer>> futures = new ArrayList<>();
for (int n = 1; n <= 3; n++) {
int number = n; // capture for the lambda
Future<Integer> f = pool.submit(() -> number * number); // βœ… each task returns a square
futures.add(f);
}
for (Future<Integer> f : futures) {
System.out.println("Result: " + f.get()); // collect each result in order
}
pool.shutdown();
}
}

A few details worth pointing out.

  • We copy n into a local number because a lambda can only use a variable that does not change.
  • We store every Future in a list, so none of them get lost.
  • The collecting loop calls get() on each Future in turn, gathering all the squares.

Output

Result: 1
Result: 4
Result: 9

This β€œsubmit all, then collect all” shape is so common that Java gives you a shortcut.

πŸ› οΈ invokeAll for a list of Callables

When you have a list of Callables and want to run them all and wait, invokeAll does it in one call. You pass a list of Callables and get back a list of Futures, one per task. By the time it returns, every task is finished, so each get() returns instantly.

This program builds three tasks and runs them all with one method call.

import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(3);
List<Callable<String>> tasks = List.of(
() -> "Alex finished",
() -> "Riya finished",
() -> "Order packed"
);
List<Future<String>> results = pool.invokeAll(tasks); // βœ… runs all, waits for all
for (Future<String> f : results) {
System.out.println(f.get()); // every task is already done here
}
pool.shutdown();
}
}

What invokeAll is doing for you.

  • It submits all the tasks at once and runs them across the pool.
  • It blocks until every task has finished, then returns the list of Futures.
  • Because all tasks are done, the get() calls in the loop never have to wait.

Output

Alex finished
Riya finished
Order packed

So invokeAll is the tidy choice for a fixed batch of work when you want all the answers before moving on.

πŸ€” What happens when a task fails

Real tasks can fail. A network call times out, a file is missing, a number is divided by zero. With Callable, the failure rides back to you through the Future. When the task throws, future.get() throws an ExecutionException, a wrapper whose β€œcause” is the real error. You unwrap it with getCause().

This task divides by zero on purpose, and get() surfaces that failure.

import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(1);
Future<Integer> future = pool.submit(() -> 10 / 0); // ❌ throws inside the task
try {
Integer result = future.get();
System.out.println("Result: " + result);
} catch (ExecutionException e) {
System.out.println("Task failed: " + e.getCause()); // βœ… the real error
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
pool.shutdown();
}
}

Two exceptions can come out of get():

  • ExecutionException means the task itself threw something. Read e.getCause() for the actual error.
  • InterruptedException means the waiting thread was interrupted while blocked. Restoring the interrupt with Thread.currentThread().interrupt() is the polite, correct response.

Output

Task failed: java.lang.ArithmeticException: / by zero

Errors in background work do not vanish. They wait in the Future until you call get(), then speak up. Handle them and your program stays in control.

βš–οΈ Callable vs Runnable

Both are tasks for threads, but they differ in two clear ways:

  • Runnable has run(), returns nothing (void), and cannot throw checked exceptions. Use it for fire-and-forget work, like logging or saving a file.
  • Callable has call(), returns a value, and can throw checked exceptions. Use it with a Future when the task must give something back, like a computed total.

Settle this before you write the task.

πŸ†• A peek at CompletableFuture

CompletableFuture is a richer future that lets you chain steps and react when a result arrives, without ever blocking on get(). You attach a follow-up action that runs automatically once the value is ready. For larger programs that string many async steps together, it is usually the better fit. Plain Callable and Future stay the simple choice for β€œrun this and fetch the answer.”

⚠️ Common Mistakes

A few Callable and Future slip-ups trip up almost everyone at first.

  • Calling get() too early. It blocks until the result is ready, so calling it right after submit throws away your parallelism.
Future<Integer> future = pool.submit(task);
Integer result = future.get(); // ❌ blocks straight away, no work done in parallel
// ... do nothing useful while waiting ...
Future<Integer> future = pool.submit(task);
doOtherWork(); // βœ… run other work while the task runs
Integer result = future.get(); // βœ… now collect, with the wait already shortened
  • Not handling ExecutionException. When the task fails, get() throws ExecutionException. Catching only the wrong type, or ignoring it, hides real failures.
Integer result = future.get(); // ❌ unhandled checked exceptions, fails to compile or hides errors
try {
Integer result = future.get();
} catch (ExecutionException e) {
System.out.println("Task failed: " + e.getCause()); // βœ… read the real cause
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // βœ… restore the interrupt flag
}
  • Forgetting to shut down the pool. A Callable still runs on an executor. If you never call shutdown, the pool’s threads keep the program alive and it may never exit.
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(task);
// ❌ no shutdown, so the program can hang at the end
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(task);
pool.shutdown(); // βœ… pool can finish and the program can exit

βœ… Best Practices

Habits that keep your Callable and Future code clean and fast.

  • Use Callable when a task returns a value. Use Runnable when it does not. Pick this before you write the task body.
  • Submit, then do other work, then get(). Call get() only when you truly need the result, so the background work overlaps with your main thread.
  • Check isDone() if you do not want to wait. It tells you whether the result is ready without blocking, which is handy in a loop.
  • Use a timeout with get(timeout, unit). A slow task should not freeze your program forever. A bounded wait keeps you in control.
  • Always handle ExecutionException. Read e.getCause() so the real failure is visible, not swallowed.
  • Still shut down the pool. Callable runs on an executor, which you must shut down so the program can end cleanly.

🧩 What You’ve Learned

Great, that completes the Multithreading module. Let’s recap Callable and Future.

  • βœ… Callable is a task that returns a value (via call) and can throw checked exceptions, unlike Runnable which returns nothing.
  • βœ… Submitting a Callable to an executor gives back a Future, a claim ticket for the not-yet-ready result.
  • βœ… future.get() fetches the result, blocking until it is ready, and throws ExecutionException if the task failed.
  • βœ… Submit the task, do other work, then call get() to make the most of parallelism.
  • βœ… invokeAll runs a whole list of Callables and waits for all of them, and CompletableFuture is the modern, non-blocking alternative.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    How does Callable differ from Runnable?

    Why: Callable returns a result; Runnable's run is void and returns nothing.

  2. 2

    What does submitting a Callable to an executor return?

    Why: submit returns a Future immediately, which will hold the result once the task finishes.

  3. 3

    What does future.get() do?

    Why: get() blocks the calling thread until the result is available, then returns it.

  4. 4

    When should you call future.get()?

    Why: Since get() blocks, do other work first and call it only when you need the result, to keep parallelism.

πŸš€ What’s Next?

You now know how to run tasks that return a result. Next we look at a richer, non-blocking way to chain async steps together so you never have to sit and wait on get().

Java CompletableFuture

Share & Connect