How to Use Guava's ListenableFuture: A Complete Guide to Async Callbacks and Composition

Guava's ListenableFuture is a thin extension of java.util.concurrent.Future that lets you register callbacks (listeners) to run automatically when an asynchronous computation finishes, enabling non-blocking pipeline construction.

Guava's concurrency utilities in the google/guava repository provide a robust alternative to standard Java futures for building reactive applications. This guide demonstrates how to use Guava's ListenableFuture interface and its supporting utilities to create, transform, and combine asynchronous tasks without blocking threads.

Core Architecture and Source Files

The ListenableFuture implementation spans several key files in com/google/common/util/concurrent/:

  • ListenableFuture.java – The interface that defines addListener(Runnable, Executor), allowing listener registration on any future implementation.
  • Futures.java – The central utility class containing static methods for transformation, combination, and callback management.
  • SettableFuture.java – A concrete mutable implementation that lets you manually complete a future with a result or exception.
  • TrustedListenableFutureTask.java – An internal bridge that adapts Callable or AsyncCallable tasks into ListenableFuture instances.
  • MoreExecutors.java – Provides listeningDecorator() to convert standard ExecutorService instances into ListeningExecutorService.

Creating ListenableFuture Instances

You cannot instantiate ListenableFuture directly; instead, use the factory methods and decorators provided by Guava.

Wrapping ExecutorService with listeningDecorator

The most common entry point is MoreExecutors.listeningDecorator(), which wraps any ExecutorService to return ListenableFuture from its submit() methods.

import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;

ListeningExecutorService listeningPool = 
    MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(4));

ListenableFuture<String> future = listeningPool.submit(() -> {
    Thread.sleep(200);
    return "Task complete";
});

As implemented in MoreExecutors.java, this decorator ensures all submitted tasks return a ListenableFuture that supports the listener protocol.

Using SettableFuture for Manual Completion

When you need to complete a future from non-executor code—such as in tests or callback-based APIs—use SettableFuture.create().

SettableFuture<String> manualFuture = SettableFuture.create();

// Later, from any thread:
manualFuture.set("Success");
// Or: manualFuture.setException(new IOException("Failed"));

The SettableFuture.java implementation provides a thread-safe, lock-free mechanism for manual completion, making it ideal for test doubles.

Immediate Futures

For constant values or known exceptions, use Futures.immediateFuture() and Futures.immediateFailedFuture() to create already-resolved instances without executor overhead.

Registering Listeners and Callbacks

The primary advantage of ListenableFuture over standard Future is the ability to react to completion without blocking.

The addListener Method

The core method defined in ListenableFuture.java is:

void addListener(Runnable listener, Executor executor);

If the future is already complete, the listener executes immediately on the calling thread (or the provided executor, depending on the implementation). Otherwise, Guava stores the listener in a lock-free queue and executes it exactly once upon completion.

future.addListener(() -> {
    try {
        System.out.println("Result: " + future.get());
    } catch (Exception e) {
        e.printStackTrace();
    }
}, MoreExecutors.directExecutor());

Note: MoreExecutors.directExecutor() runs the listener on the completing thread—use with caution for long-running tasks.

Using FutureCallback for Success and Failure Handling

Rather than writing try-catch blocks in every listener, use Futures.addCallback() defined in Futures.java to register a FutureCallback with separate onSuccess and onFailure methods.

Futures.addCallback(future, new FutureCallback<String>() {
    @Override
    public void onSuccess(String result) {
        System.out.println("Success: " + result);
    }
    
    @Override
    public void onFailure(Throwable t) {
        System.err.println("Failed: " + t.getMessage());
    }
}, listeningPool);

Transforming and Chaining Futures

Guava provides functional composition methods that avoid blocking on intermediate results.

Synchronous Transformation

Futures.transform() applies a function to the result when it becomes available, returning a new ListenableFuture.

ListenableFuture<Integer> initial = listeningPool.submit(() -> 42);

ListenableFuture<String> transformed = 
    Futures.transform(initial, 
        i -> "The answer is " + i, 
        MoreExecutors.directExecutor());

Asynchronous Transformation

When the transformation itself returns a future, use Futures.transformAsync() (formerly transform with an AsyncFunction) to flatten the nested structure.

ListenableFuture<Long> asyncTransformed = 
    Futures.transformAsync(transformed,
        s -> listeningPool.submit(() -> s.length() * 1000L),
        listeningPool);

These transformation methods, implemented in Futures.java, propagate cancellation and exceptions automatically through the chain.

Combining Multiple Futures

Aggregate multiple concurrent operations using combination utilities:

  • Futures.allAsList() – Returns a ListenableFuture<List<T>> that succeeds only if all input futures succeed; fails immediately if any input fails.
  • Futures.whenAllSucceed() – Similar to allAsList but allows you to specify a combiner function.
  • Futures.inCompletionOrder() – Returns futures in the order they complete, useful for racing queries.
ListenableFuture<String> f1 = listeningPool.submit(() -> "A");
ListenableFuture<String> f2 = listeningPool.submit(() -> "B");

ListenableFuture<List<String>> all = Futures.allAsList(f1, f2);

Futures.addCallback(all, new FutureCallback<List<String>>() {
    @Override
    public void onSuccess(List<String> results) {
        results.forEach(System.out::println);
    }
    
    @Override
    public void onFailure(Throwable t) {
        System.err.println("One task failed: " + t);
    }
}, listeningPool);

Summary

  • ListenableFuture extends standard Future with addListener() for callback-based completion handling.
  • Create instances by wrapping executors with MoreExecutors.listeningDecorator(), using SettableFuture.create() for manual control, or calling utility methods in Futures.java.
  • Register callbacks with Futures.addCallback() to handle success and failure paths separately without blocking.
  • Chain dependent work using Futures.transform() for synchronous mappings and Futures.transformAsync() for asynchronous mappings.
  • Combine concurrent operations with Futures.allAsList() or Futures.whenAllSucceed() to aggregate results.

Frequently Asked Questions

What is the difference between Future and ListenableFuture?

java.util.concurrent.Future only provides blocking access to results via get(). ListenableFuture adds the addListener() method, allowing you to register callbacks that execute automatically upon completion. This enables reactive programming patterns where downstream processing triggers immediately without consuming threads waiting on get().

When should I use SettableFuture?

Use SettableFuture when you need to complete a future manually from arbitrary code rather than from an executor submission. Common scenarios include adapting legacy callback-based APIs, creating test doubles for unit tests, or implementing promise-like patterns where the computation result is supplied by external events.

How does error handling work with ListenableFuture callbacks?

Exceptions propagate through transformations and callbacks automatically. When using Futures.addCallback(), the onFailure() method receives the exception. In transformation chains (transform or transformAsync), if an input future fails, the output future fails immediately with the same exception without executing the transformation function. Always handle exceptions in the onFailure callback or catch ExecutionException when calling get().

Is ListenableFuture better than Java 8's CompletableFuture?

ListenableFuture predates CompletableFuture and offers tighter integration with Guava's ecosystem (caching, service utilities, and Dagger Producers). While CompletableFuture provides more flexible composition APIs, ListenableFuture remains advantageous when working with legacy Guava-based codebases or when you need the specific aggregation utilities in Futures.java. Both interfaces solve similar problems but have different method signatures and ecosystem constraints.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →