How to Add Asynchronous Timeout to CompletableFuture in JDK 8

This article explains why JDK 8's CompletableFuture lacks built‑in timeout interruption, analyzes common usage patterns and their limitations, and presents a custom asynchronous timeout solution that works in both JDK 8 and JDK 9 environments, complete with reusable utility code.

JD Cloud Developers
JD Cloud Developers
JD Cloud Developers
How to Add Asynchronous Timeout to CompletableFuture in JDK 8

Introduction

JDK 8's CompletableFuture lacks built‑in timeout interruption. Existing solutions rely on the task itself to enforce a timeout. This article proposes an asynchronous timeout implementation to solve that limitation.

Preface

JDK 8 introduced many features, among them CompletableFuture, which brings event‑driven asynchronous programming to the JDK and fixes the shortcomings of Future. In everyday optimization we often use CompletableFuture for parallel execution.

Common Usage

Example scenario: two RPC calls need to be executed and their results combined.

public static void main(String[] args) {
    // Task A, takes 2 seconds
    int resultA = compute(1);
    // Task B, takes 2 seconds
    int resultB = compute(2);

    // Subsequent business logic
    System.out.println(resultA + resultB);
}

Serial execution would take at least 4 seconds because task B does not depend on task A.

Parallel version:

public static void main(String[] args) {
    // Simple demo – not production ready!
    time(() -> {
        CompletableFuture<Integer> result = Stream.of(1, 2)
            // create async tasks
            .map(x -> CompletableFuture.supplyAsync(() -> compute(x), executor))
            // combine
            .reduce(CompletableFuture.completedFuture(0),
                    (x, y) -> x.thenCombineAsync(y, Integer::sum, executor));

        // wait for result
        try {
            System.out.println("Result: " + result.get());
        } catch (ExecutionException | InterruptedException e) {
            System.err.println("Task execution exception");
        }
    });
    // Output shows both tasks start together and finish in ~2 seconds
}

Analysis

Although CompletableFuture seems to meet our needs, real‑world cases expose shortcomings. If compute(x) sometimes takes from 0.5 s to an unbounded time, we must abandon results that exceed a deadline to keep the service responsive.

Using get(long timeout, TimeUnit unit) can enforce a timeout, but the longest‑running RPC still determines overall latency.

public static void main(String[] args) {
    // Simple demo – not production ready!
    time(() -> {
        List<CompletableFuture<Integer>> result = Stream.of(1, 2)
            // create async tasks, compute(x) may timeout
            .map(x -> CompletableFuture.supplyAsync(() -> compute(x), executor))
            .toList();

        int res = 0;
        for (CompletableFuture<Integer> future : result) {
            try {
                res += future.get(2, SECONDS);
            } catch (ExecutionException | InterruptedException | TimeoutException e) {
                System.err.println("Task execution exception or timeout");
            }
        }
        System.out.println("Result: " + res);
    });
}

By setting a timeout on each compute(x) and using get or getNow, we can control the total elapsed time.

Existing Approaches

When the async task is an RPC, we can set a JSF timeout; for R2M requests we can limit the connection timeout. Both rely on third‑party middleware, which may not handle combined I/O + local computation cases.

Example of JSF timeout implementation (simplified):

public V get(long timeout, TimeUnit unit) throws InterruptedException {
    timeout = unit.toMillis(timeout);
    long remaintime = timeout - (this.sentTime - this.genTime);
    if (remaintime <= 0L) {
        if (this.isDone()) {
            return this.getNow();
        }
    } else if (this.await(remaintime, TimeUnit.MILLISECONDS)) {
        return this.getNow();
    }
    this.setDoneTime();
    throw this.clientTimeoutException(false);
}

Solution

JDK 9

JDK 9 adds orTimeout and completeTimeout to CompletableFuture, which schedule a timeout task that throws an exception if the original task does not finish in time.

public CompletableFuture<T> orTimeout(long timeout, TimeUnit unit) {
    if (unit == null) throw new NullPointerException();
    if (result == null)
        whenComplete(new Canceller(Delayer.delay(new Timeout(this), timeout, unit)));
    return this;
}

The implementation creates a Timeout runnable that completes the future exceptionally after the delay, and a Delayer singleton scheduler that runs it.

static final class Timeout implements Runnable {
    final CompletableFuture<?> f;
    Timeout(CompletableFuture<?> f) { this.f = f; }
    public void run() {
        if (f != null && !f.isDone())
            f.completeExceptionally(new TimeoutException());
    }
}
static final class Delayer {
    static ScheduledFuture<?> delay(Runnable command, long delay, TimeUnit unit) {
        return delayer.schedule(command, delay, unit);
    }
    static final class DaemonThreadFactory implements ThreadFactory {
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setDaemon(true);
            t.setName("CompletableFutureDelayScheduler");
            return t;
        }
    }
    static final ScheduledThreadPoolExecutor delayer;
    static {
        delayer = new ScheduledThreadPoolExecutor(1, new DaemonThreadFactory());
        delayer.setRemoveOnCancelPolicy(true);
    }
}

A Canceller BiConsumer cancels the scheduled timeout when the original task completes successfully.

static final class Canceller implements BiConsumer<Object, Throwable> {
    final Future<?> f;
    Canceller(Future<?> f) { this.f = f; }
    public void accept(Object ignore, Throwable ex) {
        if (ex == null && f != null && !f.isDone())
            f.cancel(false);
    }
}

JDK 8

If you are on JDK 8, you can implement the same logic yourself. The article provides a utility class CompletableFutureExpandUtils that offers an orTimeout method using the same Timeout, Delayer, and Canceller components.

public static <T> CompletableFuture<T> orTimeout(
        CompletableFuture<T> future, long timeout, TimeUnit unit) {
    if (unit == null) throw new UncheckedException("Time unit cannot be null");
    if (future == null) throw new UncheckedException("Future cannot be null");
    if (future.isDone()) return future;
    return future.whenComplete(
        new Canceller(Delayer.delay(new Timeout(future), timeout, unit)));
}

Conclusion

In JDK 8 environments, existing timeout mechanisms depend on the task itself and may fail when the task’s own timeout is ineffective. The presented approach gives CompletableFuture a reliable asynchronous timeout capability that works independently of the task implementation.

Diagram
Diagram
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

javabackend-developmentconcurrencyCompletableFutureJDK8Async Timeout
JD Cloud Developers
Written by

JD Cloud Developers

JD Cloud Developers (Developer of JD Technology) is a JD Technology Group platform offering technical sharing and communication for AI, cloud computing, IoT and related developers. It publishes JD product technical information, industry content, and tech event news. Embrace technology and partner with developers to envision the future.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.