Skip to content
Community

EXPLORE TOPICS

Editorial approach

New to CoreTrailKit?

Observe your services. Keep the evidence close.

CoreTrailKit
Back to articles
GUIDE/ Java

Java async: surface CompletableFuture failures

A background task fails, but the caller reports only that work was queued. CompletableFuture stores exceptional completion; scheduling an async task alone does not make its result part of the caller’s success decision.

By CoreTrailKit3 min read

01

Distinguish submission from completion

Identify what the caller’s response means. A queue-acceptance response can be valid if the system deliberately exposes later task status. It becomes misleading when downstream code treats that response as evidence that the work finished successfully.

Our first fixture submitted a task that threw IllegalStateException and printed a queued message. After the executor finished, a test-only status check confirmed exceptional completion. There was no success result simply because the submission call returned.

02

Keep the futures at the ownership boundary

Retain a result handle for the work whose outcome matters. If several tasks contribute to a batch, decide what constitutes batch success and how each failure is surfaced. Unrelated work should not be accidentally included merely to make a batch wait.

For CompletableFuture.allOf, the combined future tracks completion of its inputs and can complete exceptionally. It does not collect every task’s value, automatically roll back successful tasks, or make their external effects atomic.

03

Surface the cause in a controlled reproduction

Save this diagnostic program as AsyncFailureProbe.java. It intentionally handles the expected failure so assertions can inspect the cause and the successful sibling. Its output labels the batch as failed.

In application code, connect that failure to the actual response or job status. Logging an exception while still returning an ordinary success result is a separate decision and is not fixed by merely adding the catch block shown in a probe.

java · EXAMPLE
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executors;

public class AsyncFailureProbe {
    public static void main(String[] args) {
        try (var pool = Executors.newFixedThreadPool(2)) {
            CompletableFuture<String> failed = CompletableFuture.supplyAsync(() -> {
                throw new IllegalStateException("downstream unavailable");
            }, pool);
            var successful = CompletableFuture.supplyAsync(() -> "ready", pool);
            try {
                CompletableFuture.allOf(failed, successful).join();
                throw new AssertionError("Failure was hidden");
            } catch (CompletionException error) {
                if (!(error.getCause() instanceof IllegalStateException)) throw error;
                System.out.println("batch failed=" + error.getCause().getMessage());
            }
            if (!successful.join().equals("ready")) throw new AssertionError();
        }
    }
}

04

Verify both failure and surviving work

Compile and run the probe. It should print batch failed=downstream unavailable. The assertions also check that the cause is IllegalStateException and the successful sibling’s result remains ready.

The fixture uses a locally owned executor and waits for finite local tasks. Do not generalize its behavior to network deadlines or assume allOf cancels outstanding work when one input fails.

sh · EXAMPLE
javac -d out AsyncFailureProbe.java
java -cp out AsyncFailureProbe

05

Choose observation appropriate to the application

Blocking with join can be appropriate in a small command-line test, but an event-driven server may need composed stages and explicit failure handling without blocking a request thread. The key requirement is an owned, visible result path.

Test the caller’s final status under a failing dependency, not only a log line. Preserve the cause, affected operation identity, and expected handling. Separately verify retry, timeout, idempotency, and cancellation behavior where the real workflow requires them.

Know the limits

Reproduced with Eclipse Temurin OpenJDK 25.0.4.1+1 in a disposable Linux container with no external network. An ignored future completed exceptionally after its caller reported queued work. Observing allOf(...).join exposed CompletionException with the intended cause; a successful sibling remained successful. Real service timeouts, cancellation propagation, and external side effects were not tested.

Sources & further reading

A technical publication by CoreTrailKit.

Browse all articles

CONTINUE EXPLORING