DEV Community

Jagdish Salgotra
Jagdish Salgotra

Posted on

Timeout Patterns in Java 21: Full Failure vs Partial Results

Note
This article uses the Java 21 preview structured concurrency API (StructuredTaskScope, JEP 453). API shape changed in later previews. See Part 9 for Java 21 -> Java 25 migration guidance. Compile and run with --enable-preview.

Originally published on engnotes.dev:
Timeout Patterns in Java 21

This is a shortened version with the same core code and takeaways.

Timeout handling gets oversimplified pretty often.

The real design question is usually not “did we hit the deadline?” It is “what should the response mean once we do?” Some endpoints should fail completely. Others should return what is ready and mark the rest as missing. Structured concurrency helps, but it does not make that decision for you.

The Strict Version

This is the simple all-or-nothing pattern from the article:

public <T> T runInScopeWithTimeout(Callable<T> task, Duration timeout) throws Exception {
    Instant deadline = Instant.now().plus(timeout);

    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        var future = scope.fork(task);
        scope.join();

        if (Instant.now().isAfter(deadline)) {
            throw new TimeoutException("Operation exceeded timeout: " + timeout);
        }

        scope.throwIfFailed();
        return future.get();
    }
}
Enter fullscreen mode Exit fullscreen mode

This is usually the right choice when the whole response has to be correct or not returned at all.

The Partial-Results Version

When some sections are optional, the response policy changes:

public <T> List<Optional<T>> executeWithPartialResults(List<Callable<T>> tasks, Duration timeout)
        throws InterruptedException {
    Instant deadline = Instant.now().plus(timeout);

    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        List<StructuredTaskScope.Subtask<T>> subtasks = new ArrayList<>();
        for (Callable<T> task : tasks) {
            subtasks.add(scope.fork(task));
        }

        try {
            scope.joinUntil(deadline);
        } catch (TimeoutException ignored) {
            // Deadline reached: return what is available.
        }

        List<Optional<T>> results = new ArrayList<>(subtasks.size());
        for (var subtask : subtasks) {
            if (subtask.state() == StructuredTaskScope.Subtask.State.SUCCESS) {
                results.add(Optional.of(subtask.get()));
            } else {
                results.add(Optional.empty());
            }
        }

        scope.shutdown();
        return results;
    }
}
Enter fullscreen mode Exit fullscreen mode

That is the part I like here. The timeout policy is explicit. You can tell from the code whether the endpoint is strict or whether it is allowed to degrade.

The Important Java 21 Caveat

joinUntil(...) only tells you the deadline was reached. It does not decide your response policy for you.

That means you still need to answer a few questions yourself:

  • should the endpoint fail completely?
  • should it return partial data?
  • how should missing sections be represented?
  • should unfinished work be cancelled right away?

That last point matters more than people think. In the Java 21 preview style, if you return early with partial data, remember scope.shutdown(). Forgetting that is a good way to leave stale work running after the response is already gone.

When To Choose Which

I would use full failure when:

  • the response only makes sense if all parts are present
  • downstream decisions depend on complete data
  • partial data would be misleading

I would consider partial results when:

  • some fields are optional or just enrichments
  • degraded output is acceptable
  • the response contract clearly marks what is missing

That decision is usually more important than the concurrency primitive itself.

The Practical Takeaway

What structured concurrency gives you here is not “automatic timeout design.” It gives you a cleaner place to express timeout design.

That is useful because the code stops hiding the response policy. You can see whether the endpoint is strict, whether it degrades, and whether unfinished work gets cleaned up properly.

Full article with more examples, progressive callbacks, test guidance, runnable repo, and live NoteSensei chat:

Timeout Patterns in Java 21

Top comments (0)