Structured Concurrency
Structured concurrency, delivered as an incubating/preview API across many JDK releases starting with JEP 428 in Java 19 and still in preview as of Java 25's JEP 505 (continuing to evolve in subsequent releases), provides the StructuredTaskScope class to treat a group of related concurrent subtasks — typically run on virtual threads — as a single, lexically-scoped unit of work, where every forked subtask is guaranteed to complete (or be cancelled) before the scope itself exits, and errors from any subtask are handled centrally rather than needing to be separately propagated and reasoned about. This directly addresses the reliability and observability problems of unstructured concurrency, where a plain ExecutorService or raw thread creation allows subtasks to silently outlive the logical operation that spawned them, with no enforced relationship between a task's lifetime and its subtasks' lifetimes. This entry covers exactly what unstructured concurrency gets wrong and why virtual threads made the problem more visible, the StructuredTaskScope.open()/fork()/join() lifecycle and Joiner-based completion policies, and how subtask cancellation and the strict thread-ownership/nesting rules work. As of Java 25, this remains a preview feature requiring --enable-preview, with the public-constructor-based API of earlier previews replaced by static factory methods.
Motivation — What Unstructured Concurrency Gets Wrong
// ── Unstructured concurrency — nothing enforces subtask lifetime/cleanup
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<String> userFuture = executor.submit(() -> fetchUser(userId));
Future<List<Order>> ordersFuture = executor.submit(() -> fetchOrders(userId));
try {
String user = userFuture.get(); // if THIS throws...
List<Order> orders = ordersFuture.get(); // ...ordersFuture's task keeps running anyway —
return combine(user, orders); // nothing automatically cancelled it
} catch (Exception e) {
// Must manually remember to cancel the OTHER future here, at every call site like this:
ordersFuture.cancel(true);
throw e;
}
// Easy to forget this manual cancellation — and even when remembered, it has to be
// hand-written and correctly maintained separately at every concurrent call site
// ── Structured concurrency — the scope's own exit GUARANTEES subtask cleanup
// (preview feature — requires --enable-preview as of Java 25)
String result;
try (var scope = StructuredTaskScope.open()) {
StructuredTaskScope.Subtask<String> user = scope.fork(() -> fetchUser(userId));
StructuredTaskScope.Subtask<List<Order>> orders = scope.fork(() -> fetchOrders(userId));
scope.join(); // waits for both — if EITHER fails, the other is automatically cancelled,
// and join() throws — no manual cancellation logic needed anywhere
result = combine(user.get(), orders.get());
}
// By the time this try-with-resources block exits, BOTH subtasks are guaranteed to have
// either completed or been cancelled — no thread is ever left running in the backgroundThe open()/fork()/join() Lifecycle and Joiner-Based Completion Policies
// (preview feature — requires --enable-preview as of Java 25; exact API may still evolve)
// ── open() — the default, fail-fast policy ──────────────────────────────
try (var scope = StructuredTaskScope.open()) {
StructuredTaskScope.Subtask<String> productInfo = scope.fork(() -> fetchProductInfo(productId));
StructuredTaskScope.Subtask<Integer> inventory = scope.fork(() -> fetchInventory(productId));
StructuredTaskScope.Subtask<Double> price = scope.fork(() -> fetchPersonalizedPrice(userId, productId));
scope.join(); // default policy: if ANY subtask fails, the others are cancelled
// and join() throws — surfaces the failure, doesn't silently continue
// Reaching here means ALL THREE subtasks succeeded:
return new ProductPage(productInfo.get(), inventory.get(), price.get());
}
// ── Only the scope's OWNER thread may fork or join — enforced at runtime ─
try (var scope = StructuredTaskScope.open()) {
Thread.ofVirtual().start(() -> {
// scope.fork(() -> "illegal"); // StructureViolationException — wrong thread
}).join();
}
// ── Custom Joiner — e.g. "first successful result wins, cancel the rest" ─
// Used for redundant-source patterns: query several equivalent backends,
// take whichever responds first, cancel the no-longer-needed remainder:
try (var scope = StructuredTaskScope.open(StructuredTaskScope.Joiner.<String>anySuccessfulResultOrThrow())) {
scope.fork(() -> queryPrimaryRegion());
scope.fork(() -> querySecondaryRegion());
scope.fork(() -> queryTertiaryRegion());
String fastestResult = scope.join(); // returns as soon as ONE succeeds;
// the still-running others are automatically cancelled at that point
return fastestResult;
}Cancellation, Nesting, and Strict Structural Enforcement
// ── Cancellation propagates automatically to sibling subtasks on failure ─
try (var scope = StructuredTaskScope.open()) {
var risky = scope.fork(() -> { throw new RuntimeException("failed"); });
var slow = scope.fork(() -> {
Thread.sleep(Duration.ofSeconds(30)); // this subtask gets INTERRUPTED/cancelled
return "never reached"; // as soon as "risky" fails above
});
scope.join(); // throws here, surfacing "risky"'s failure — "slow" has ALREADY
// been cancelled by this point, not left running in the background
}
// ── Nested scopes — hierarchy mirrors the code's own block structure ───
try (var outerScope = StructuredTaskScope.open()) {
var aggregateTask = outerScope.fork(() -> {
try (var innerScope = StructuredTaskScope.open()) {
var sub1 = innerScope.fork(() -> fetchPartA());
var sub2 = innerScope.fork(() -> fetchPartB());
innerScope.join();
return combine(sub1.get(), sub2.get());
}
// innerScope's subtasks are GUARANTEED done by this point — nested,
// matching the lexical nesting of outerScope -> aggregateTask -> innerScope
});
outerScope.join();
System.out.println(aggregateTask.get());
}
// A thread dump taken during this execution shows the full scope hierarchy directly —
// outerScope's subtask, and innerScope nested beneath it, with their forked threads —
// rather than a flat, unstructured list of unrelated-looking thread names
// ── Strict structural enforcement — violations throw at run time ──────
// StructuredTaskScope leaked = ...;
// void useOutsideItsBlock() {
// leaked.fork(() -> "too late"); // StructureViolationException —
// // using a scope outside the try-with-resources block that opened it
// // is rejected, not merely discouraged by convention
// }