Java 25 arrives with Structured Concurrency in its fifth preview and reorganizes concurrent failure handling
JEP 505 replaces ExecutorService and Future with StructuredTaskScope opened via factory methods. See the code before and after, and what's still preview.

Java 25 shipped as an LTS release and brought, among other things, the fifth preview of Structured Concurrency via JEP 505. The API isn't new: it incubated back in JDK 19 and has been repreviewed release after release, but the current round changes the main signature, and it's worth understanding what changes for anyone running Java services in production doing fan-out of I/O calls.
The problem the proposal tackles is old and familiar to anyone who has ever orchestrated concurrent calls by hand: ExecutorService and Future give too much freedom and too little structure. You submit tasks, hold onto the futures, call get() on each one, and the logical relationship between the parent task and its subtasks exists only in your head, not in the runtime.
What breaks with ExecutorService
The canonical example from the JEP itself is a handler that fetches user and order in parallel:
Response handle() throws ExecutionException, InterruptedException {
Future<String> user = executor.submit(() -> findUser());
Future<Integer> order = executor.submit(() -> fetchOrder());
String theUser = user.get(); // Join findUser
int theOrder = order.get(); // Join fetchOrder
return new Response(theUser, theOrder);
}It looks harmless, but the JEP lists three concrete pitfalls:
- If
findUser()throws an exception,handle()fails atuser.get(), butfetchOrder()keeps running on its own thread. That's a thread leak: at best it wastes resources, at worst it interferes with other tasks. - If the
handle()thread is interrupted, the interruption doesn't propagate to the subtasks. Both leak. - If
findUser()takes a long time andfetchOrder()fails partway through,handle()stays blocked onuser.get()waiting pointlessly, because the order of theget()calls is fixed and doesn't react to the other subtask's failure.
You can patch all of this with try-finally, cancel(boolean) on the futures inside the catch, and ExecutorService inside try-with-resources. But, as the JEP text acknowledges, this juggling of manually coordinating lifetimes "can be difficult to get right, and often makes the logical intent of the code harder to discern." The root cause is that a Future can be joined by any thread that holds a reference to it, including one that never submitted anything, so the runtime has no way to enforce a parent-child relationship.
What it looks like with StructuredTaskScope
The same logic with the new API:
Response handle() throws InterruptedException {
try (var scope = StructuredTaskScope.open()) {
Subtask<String> user = scope.fork(() -> findUser());
Subtask<Integer> order = scope.fork(() -> fetchOrder());
scope.join(); // Join the subtasks, propagating exceptions
return new Response(user.get(), order.get());
}
}The central difference: the threads' lifetime is confined to the try-with-resources block. Outside of it, they don't exist. And StructuredTaskScope.open() with no parameters already comes with the default policy that solves the three pitfalls at once:
- Short-circuiting: if
findUser()orfetchOrder()fails, the other one is automatically canceled (interrupted), if it hasn't finished yet. - Cancellation propagation: if the
handle()thread is interrupted before or duringjoin(), both subtasks are canceled when the scope exits. - Observability: a thread dump shows
findUser()andfetchOrder()as children of the scope, not dangling on loose threads with no relationship at all.
Notice that fork() now returns a Subtask, not a Future (a change that came back in JDK 21). And Subtask::get() can only be called after join(), otherwise it throws an exception. This forces the correct flow: fork everything, join as a unit, only then read the result.
This preview's change: factory methods and Joiner
What JEP 505 changes relative to the fourth preview (JEP 499) is how the scope is opened. Before, you used public constructors; now, static factory methods:
public static <T> StructuredTaskScope<T, Void> open();
public static <T, R> StructuredTaskScope<T, R> open(Joiner<? super T, ? extends R> joiner);The zero-parameter open() covers the common case (wait for all subtasks to succeed, or fail if any one fails). For other policies, you pass a Joiner to the one-parameter version. That's where the alternative behaviors live: waiting for the first successful result and canceling the rest, collecting all successes while ignoring failures, and so on. The API became parameterized as , where T is the subtasks' type and R is the type of the join() result, which gives more expressiveness than the previous version based on fixed subclasses.
Each fork() starts a thread that, by default, is a virtual thread (JEP 444). It's the combination that gives the whole thing meaning: virtual threads make it cheap to dedicate a thread per I/O operation, and Structured Concurrency coordinates that swarm without leaks. A subtask can open its own scope and fork its own subtasks, forming a hierarchy of scopes that mirrors the code's syntactic nesting, the concurrent equivalent of a single thread's call stack.
What's already safe to use and what isn't
Here comes the pragmatic message. StructuredTaskScope remains a preview API, disabled by default. To compile and run it, you need the flags:
javac --release 25 --enable-preview Main.java
java --enable-preview MainOr, with the source launcher, java --enable-preview Main.java; in jshell, jshell --enable-preview.
In practice, this means the signature can still change, and it did change between previews (this round swapped constructors for factory methods; the documentation already points to JEP 525 as the sixth preview). Putting this on the critical path of a production service today is a bet that your codebase will keep up with refactors at every release, with no guarantee of binary compatibility. For production code that needs stability, the pair ExecutorService + manual coordination, as tedious as it is, remains the supported path.
Where the preview pays off: internal projects, POCs, new code in teams that already run an up-to-date Java and want to get ahead of a standard that will almost certainly become final, and I/O fan-out scenarios where the old model's thread leak already causes real pain. The gain in readability and observability is concrete, and migrating later tends to be mechanical.
One point of attention the JEP itself raises: cancellation only works if subtasks respond to interruption. If a subtask blocks in a non-interruptible method, it can hold up the scope's close() indefinitely, because close() always waits for all threads to finish, even with the scope canceled. In other words, the structure solves the coordination, but it doesn't fix code that ignores InterruptedException.
It's worth reinforcing what the proposal is not meant to do: it isn't meant to replace ExecutorService or Future, which continue to exist for cases of unstructured concurrency. It isn't a data channel between threads, and it doesn't replace the interruption mechanism. It's a tool for a specific pattern: a task that splits into subtasks and joins them in the same block. Within that scope, it's where it makes the most sense, and outside of it, the old constructs remain the right choice.
Translated from the Brazilian Portuguese original · Read the original
CodeQL 2.27.1 gets C/C++ queries and Kotlin 2.4.20 support
The version released on September 25, 2026 refines GitHub's static analysis engine with new taint flow models for C/C++, adjustments to Kotlin's K2 compiler, and fixes that reduce false positives across several languages.
