⏱️ Section 19 · Question #18

How to cancel CompletableFuture execution

To cancel a computation in CompletableFuture, invoke the cancel(boolean mayInterruptIfRunning) method inherited from the Future interface.


🟢 Junior Level

To cancel a computation in CompletableFuture, invoke the cancel(boolean mayInterruptIfRunning) method inherited from the Future<T> interface.

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(5000); // Simulates long-running task
    } catch (InterruptedException e) {
        System.out.println("Interrupted!");
    }
    return "Finished";
});

// Cancel the future:
boolean cancelled = future.cancel(true);

System.out.println("isCancelled: " + future.isCancelled()); // true
System.out.println("isDone: " + future.isDone());           // true

[!WARNING] The primary trap of CompletableFuture: invoking cancel(true) DOES NOT INTERRUPT the underlying worker thread executing the task! The runtime completely ignores the mayInterruptIfRunning parameter.

Attempting to retrieve the result of a cancelled future via .get() or .join() throws java.util.concurrent.CancellationException.


🟡 Middle Level

Why cancel() Does Not Interrupt the Thread

In the legacy Java 5 FutureTask, calling cancel(true) captures the executing thread reference and invokes thread.interrupt().

In CompletableFuture, the architecture is fundamentally different:

  1. CompletableFuture is not a task (Runnable), but a completion promise (state descriptor). It can be completed by a network I/O callback, a timer, or an external event loop rather than an executor thread.
  2. In the JDK source code:
    public boolean cancel(boolean mayInterruptIfRunning) {
        boolean cancelled = (result == null) &&
            internalComplete(new AltResult(new CancellationException()));
        postComplete();
        return cancelled || isCancelled();
    }
    

    The mayInterruptIfRunning argument is completely unused. The method merely performs an atomic CAS transition setting result to AltResult(CancellationException).

  3. The background worker thread continues executing its loop or socket read to natural completion, consuming CPU and memory.

Cooperative Cancellation Token Pattern

To stop a background task upon cancellation, implement a cooperative cancellation token:

public class CancellableWorker {
    private final AtomicBoolean isCancelled = new AtomicBoolean(false);

    public CompletableFuture<Void> doWorkAsync() {
        return CompletableFuture.runAsync(() -> {
            while (!isCancelled.get() && !Thread.currentThread().isInterrupted()) {
                processBatch();
            }
            if (isCancelled.get()) {
                System.out.println("Worker detected cancellation signal and exited gracefully.");
            }
        });
    }

    public void cancel() {
        isCancelled.set(true);
    }
}

🔴 Senior Level

Cancellation Propagation Direction (Downstream vs Upstream)

In a CompletableFuture Directed Acyclic Graph (DAG), cancellation propagates strictly downstream and never flows upstream:

CompletableFuture<String> source = CompletableFuture.supplyAsync(() -> fetchFromDb());
CompletableFuture<String> step1 = source.thenApply(s -> transform(s));
CompletableFuture<Void> step2 = step1.thenAccept(s -> save(s));

// Scenario 1: Cancelling the parent (Upstream)
source.cancel(true);
// Result: source, step1, and step2 ALL transition to CancellationException.
// Cancellation cascades downstream through the entire dependent tree!

// Scenario 2: Cancelling the leaf stage (Downstream)
step2.cancel(true);
// Result: ONLY step2 transitions to CancellationException.
// Antecedent stages 'source' and 'step1' CONTINUE EXECUTING to normal completion!

[!IMPORTANT] If a client cancels an HTTP request at the controller layer (step2.cancel()), the database query (source) is not cancelled, continuing to consume database connection pool capacity unless reverse cancellation is manually wired!

Interaction with whenComplete and handle

Cancellation transitions the future to an exceptionally completed state (isCompletedExceptionally() == true). Consequently, whenComplete and handle stages are guaranteed to execute upon cancellation, receiving an instance of CancellationException in their Throwable parameter:

CompletableFuture<Resource> resourceFuture = acquireResourceAsync();

resourceFuture.whenComplete((resource, ex) -> {
    if (ex instanceof CancellationException) {
        log.warn("Task was cancelled! Releasing acquired native resources...");
    }
    closeSilently(resource);
});

// Triggers cleanup logic in whenComplete:
resourceFuture.cancel(true);

4 Tricky Questions

1. Does invoking future.cancel(true) interrupt the worker thread running a CompletableFuture.supplyAsync task?

Answer: No, absolutely not. In CompletableFuture.cancel(boolean mayInterruptIfRunning), the boolean flag mayInterruptIfRunning is completely ignored. The method simply sets the future’s internal state to AltResult(CancellationException). No Thread.currentThread().interrupt() signal is dispatched to the executing worker thread. The thread will continue executing the supplier lambda until natural termination.

2. What exceptions are thrown when calling .join() vs .get() on a cancelled CompletableFuture? Are they wrapped?

Answer: Both .join() and .get() throw java.util.concurrent.CancellationException directly without wrapping. It is NOT wrapped in ExecutionException (for get()) or CompletionException (for join()). This is an explicit requirement of the Future specification: cancellation is treated as a top-level lifecycle event rather than an execution failure.

3. In a 3-stage pipeline f1.thenApply(...).thenApply(...), if cancel(true) is invoked on the final 3rd stage, do stages 1 and 2 abort?

Answer: No, stages 1 and 2 do not abort. CompletableFuture does not support upstream back-propagation of cancellation. Stages 1 and 2 will execute to normal completion on their respective worker threads, and only the 3rd stage will record a CancellationException. To cancel the entire pipeline, the application must retain a reference to the root stage (f1) and invoke cancel() on the root.

4. Can cf.complete(val) return true if cf was already cancelled via cf.cancel(true)?

Answer: No, complete(val) will return false. The cancel() method transitions the internal result field to AltResult(CancellationException) via an atomic CAS operation. Any subsequent call to complete(val) or completeExceptionally(ex) observes that result != null, fails the CAS operation, and returns false without modifying the future’s state.


🎯 Interview Cheat Sheet

Summary in 60 Seconds

  • cancel(boolean mayInterruptIfRunning) marks the future as cancelled, but does not interrupt the worker thread (the boolean flag is ignored).
  • Calling .get() or .join() on a cancelled future throws CancellationException directly without wrapping.
  • Cancellation propagates only downstream: cancelling a root stage cancels all children, but cancelling a child stage leaves parent stages running.
  • whenComplete and handle execute upon cancellation and receive CancellationException.
  • To actually abort executing background tasks, use cooperative cancellation tokens (AtomicBoolean polling).

Common Interview Mistakes

  • Believing cancel(true) interrupts threads via Thread.interrupt().
  • Assuming cancelling a downstream stage aborts upstream database or HTTP operations.
  • Forgetting that whenComplete runs even when a task is cancelled.