Java Callable and Future Tutorial with Examples (Java 25)

Callable and Future in Java with runnable examples of get() with timeout, exceptions, cancel(), Java 19 state() and resultNow(), and FutureTask.

Callable submitted to an ExecutorService returns a Future that moves from RUNNING to SUCCESS, FAILED or CANCELLED

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.

FeatureRunnableCallable<V>
Methodvoid run()V call() throws Exception
Returns a valueNoYes
Checked exceptionsMust be caught inside run()Can be thrown, and Future.get() reports them
Runs on a plain ThreadYes, new Thread(runnable)Only through a FutureTask
Submitted to an executorexecute() 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.

Callable submitted to an ExecutorService returns a Future that moves from RUNNING to SUCCESS, FAILED or CANCELLED
A Future starts in RUNNING and ends in exactly one of three states, which decides what get() does

The methods split into blocking calls and calls that return at once.

MethodBlocksReturns or throws
get()Yes, until the task is doneThe result, or ExecutionException, CancellationException, InterruptedException
get(timeout, unit)Up to the timeoutSame as get(), plus TimeoutException
isDone()Notrue once the task succeeded, failed or was cancelled
cancel(mayInterrupt)Nofalse if the task had already completed
isCancelled()Notrue if the task was cancelled before it completed
state() (Java 19)NoRUNNING, SUCCESS, FAILED or CANCELLED
resultNow() (Java 19)NoThe result, or IllegalStateException if the task did not succeed
exceptionNow() (Java 19)NoThe 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.

NeedFuture from submit()CompletableFuture
Get the resultget(), blocksjoin() or get(), or a callback
Run code when the result arrivesNot possible without a blocking threadthenApply(), thenAccept()
Combine two resultsCall get() on boththenCombine(), allOf()
Handle an exceptionCatch ExecutionExceptionexceptionally(), handle()
Complete it by handNocomplete(), 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

Happy Learning !!

Source Code on Github

Leave a Comment

  1. 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);
          }
      
  2. 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)?

  3. 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());
    }
    }
    }

  4. 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());
              }
      
      
  5. Hello

    How would you handle Rejected Task if i use ArrayBlockingQueue and choose to implement my own RejectedExecutionHandler Class.

    Dushyant Sapra

  6. 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.

  7. 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.

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.