Skip to Content
Course content

262: Cancelling and Timing Out Async Tasks

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine we're building a dashboard for a logistics company. One of the features is a "Real-time Route Optimizer" that hits a heavy external API to calculate the best path for a truck. The problem? Sometimes that API takes ten seconds, sometimes it takes thirty, and sometimes it just hangs. We can't let our entire UI freeze or our thread pool clog up because one external server is having a bad day.

The API that won't stop talking

I'll start with a basic CompletableFuture that simulates this slow API call. I'm using a Thread.sleep here just to mimic that agonizing wait.

CompletableFuture<String> routeTask = CompletableFuture.supplyAsync(() -> {
    try {
        System.out.println("Calculating route...");
        Thread.sleep(10000); // Simulating a 10-second lag
        return "Route Optimized: Take Highway 101";
    } catch (InterruptedException e) {
        System.out.println("Task was interrupted!");
        return "Interrupted";
    }
});

Now, what if the user clicks "Cancel" after two seconds? I'll try the most obvious approach: calling routeTask.cancel(true). I'm passing true because I want to interrupt the thread if it's already running.

// User clicks cancel after 2 seconds
Thread.sleep(2000);
routeTask.cancel(true);
System.out.println("Cancelled the task.");

When I run this, I notice something frustrating. The console says "Cancelled the task," but then... a few seconds later... "Task was interrupted!" pops up. Wait, if I cancelled it, why did it keep running for a bit? It turns out CompletableFuture.cancel() doesn't actually send a hard signal to the thread to stop immediately in all environments; it mostly just marks the future as cancelled so that anyone waiting for the result gets an exception. It's a bit "polite" for my liking.

Actually stopping the work

To make cancellation actually work, the code inside the async block has to be "interrupt-aware." Since I used Thread.sleep, it actually does respond to interrupts, but if I were doing a heavy while loop calculating coordinates, I'd need to manually check Thread.currentThread().isInterrupted().

But let's be honest: relying on the user to click "Cancel" is a gamble. We need a systemic way to say, "If this takes longer than 3 seconds, I don't care about the result anymore."

Setting a hard deadline

Java 9 introduced orTimeout(), which is exactly what I need here. Instead of manually managing a timer, I can just chain it onto the future. Let's try that.

CompletableFuture<String> routeTaskWithTimeout = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(10000); 
        return "Route Optimized";
    } catch (InterruptedException e) {
        return "Interrupted";
    }
}).orTimeout(3, TimeUnit.SECONDS); // Hard limit!

I'll wrap this in a try-catch because orTimeout doesn't just return a null; it throws a TimeoutException wrapped in an ExecutionException when you try to get the result. When I run this, the program crashes after exactly 3 seconds. It's efficient, but crashing isn't a great user experience.

A graceful fallback

I don't want an exception; I want a fallback. If the optimizer is too slow, I'll just give the user a "Standard Route" (the cached one) instead of the "Optimized Route." This is where completeOnTimeout() comes in. It's much cleaner because it provides a default value instead of throwing a tantrum.

CompletableFuture<String> finalRouteTask = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(10000);
        return "Optimized Route: Via Scenic Bypass";
    } catch (InterruptedException e) {
        return "Interrupted";
    }
}).completeOnTimeout("Standard Route: Via Main Street", 3, TimeUnit.SECONDS);

System.out.println("Result: " + finalRouteTask.join());

Now, when I run this, I wait 3 seconds, and boom: Result: Standard Route: Via Main Street. The original task might still be churning away in the background for a moment, but our main application logic has moved on. We've successfully decoupled our user experience from the instability of the external API.




📋 Practical Task

Build a Fail-Safe Weather Fetcher

You are tasked with creating a WeatherService class. The service should attempt to fetch weather data from a simulated "Slow API" that randomly takes between 1 and 10 seconds to respond.

  • Create a method fetchWeather() that returns a CompletableFuture<String>.
  • Inside the future, simulate the delay using Thread.sleep() with a random duration.
  • Use completeOnTimeout() to ensure that if the API takes longer than 2 seconds, the method returns the string: "Weather data currently unavailable - using cached data".
  • In your main method, call this service and print the result.
  • Bonus: Add a mechanism to cancel the task manually using cancel(true) if the result hasn't returned within 1 second, and observe how the InterruptedException is handled.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.