Note
This article uses Java 21 preview structured concurrency APIs (JEP 453). See Part 9 for migration changes in Java 25 preview APIs. Compile and run with--enable-preview.
Originally published on engnotes.dev:
Conditional Cancellation in Java 21
This is a shortened version with the same core code and takeaways.
Timeouts are only one kind of failure.
A lot of expensive failures happen when the outcome is already decided, but parts of the request are still doing work anyway. Payment failed. Risk check failed. Circuit breaker is already open. In those cases, letting sibling tasks continue is usually just wasted load.
That is where conditional cancellation becomes useful.
The Fail-Fast Version
One practical Java 21 pattern is to turn a terminal business failure into an exception inside the subtask, so ShutdownOnFailure can cancel sibling work:
public String circuitBreakerExample() throws Exception {
if (circuitBreakerFailures.get() > 3) {
logger.warn("Circuit breaker is OPEN - failing fast");
return "Circuit breaker is OPEN - service unavailable";
}
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var primaryService = scope.fork(() -> {
if (Math.random() < 0.3) {
circuitBreakerFailures.incrementAndGet();
throw new RuntimeException("Service failure");
}
circuitBreakerFailures.set(0);
return simulateServiceCall("primary-service", 100);
});
scope.join();
scope.throwIfFailed();
return "Circuit Breaker Result: " + primaryService.get();
}
}
What I like here is that the fail-fast rule is not buried somewhere else in the request flow. It is part of the scope behavior.
Circuit Breaker Policy Is Still Separate
The article makes an important distinction I would keep:
public String callProtectedService(String request) throws Exception {
if (dbCircuitBreaker.isOpen()) {
String message = String.format(
"Circuit breaker is OPEN for %s - failing fast (failures: %d/%d, next retry in: %s)",
dbCircuitBreaker.getServiceName(),
dbCircuitBreaker.getFailureCount(),
dbCircuitBreaker.getThreshold(),
dbCircuitBreaker.getTimeUntilRetry());
logger.warn(message);
throw new RuntimeException(message);
}
try {
String result = scopedHandler.runInScope(() -> callUnreliableService(request));
dbCircuitBreaker.onSuccess();
return result;
} catch (Exception e) {
dbCircuitBreaker.onFailure();
throw e;
}
}
That separation is worth keeping straight.
- the circuit breaker decides whether a call should even be attempted
- the structured scope manages lifecycle for work that actually starts
Mix those up too much and the code gets harder to reason about.
Fallback Should Be Intentional
Fallback is useful too, but only when degraded results are genuinely acceptable:
public <T> T runWithFallback(Callable<T> primary, Callable<T> fallback) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var primaryFuture = scope.fork(primary);
scope.join();
try {
scope.throwIfFailed();
return primaryFuture.get();
} catch (Exception e) {
logger.warn("Primary task failed, using fallback: {}", e.getMessage());
return fallback.call();
}
}
}
I would not use this as a default escape hatch for every failure. It makes sense for optional enrichments. It is a bad habit when it starts hiding broken primary paths.
The Java 21 Review Rule
For this style of code, I would still review the same basics first:
- convert terminal business failures into explicit exceptions if they should stop sibling work
- call
join()before reading results - call
throwIfFailed()beforeget - keep retries bounded and visible
- keep side effects idempotent where cancellation can happen mid-flow
Those are the details that make fail-fast code trustworthy instead of just clever.
The Practical Takeaway
What structured concurrency gives you here is a cleaner boundary for business-condition failure.
If the request is already done in business terms, the code should be able to say that clearly and stop the rest. That is a better model than letting work leak on just because the tasks were launched.
Full article with more examples, testing guidance, runnable repo, and live NoteSensei chat:
Top comments (0)