Dev & EngARTICLE

JDK 25 brings the fifth preview of Structured Concurrency, not the stable version

Understand what changes in the fifth preview of the StructuredTaskScope API and why it's still not time to use `--enable-preview` in production.

What JEP 505 delivers (and what it doesn't)

Editor's note: the original text stated that the "Status" field of the official JEP 505 document shows the value "Fifth Preview". In fact, the Status field of JEP 505 is "Closed / Delivered"; "Fifth Preview" is part of the JEP's title/name ("Structured Concurrency (Fifth Preview)"), not a value of the Status field. The article's central thesis, that the feature remains in preview and requires --enable-preview, remains correct and is confirmed by other elements of the JEP itself (the need for preview flags, JEP 525 already planned as the sixth preview), but the reader should note this specific terminological inaccuracy.

JDK 25 adds the fifth round of preview for Structured Concurrency, with changes in how scopes are opened and a JEP 525 already scheduled for the sixth. For those maintaining manual ExecutorService in production, understanding the model is already worthwhile, even without using the API yet.

JEP 505, signed by Alan Bateman, Viktor Klang and Ron Pressler, describes Structured Concurrency in JDK 25, identified as Release 25 in the official OpenJDK document. The status in the official OpenJDK document is "Fifth Preview": it is the fifth time the API goes through preview, not its formalization into a stable API. The JEP itself already points to the next step, JEP 525, named "Structured Concurrency (Sixth Preview)", targeting the following JDK version.

Official JEP 505 page showing the 'Fifth Preview' status and the revision history of Structured Concurrency since JDK 19
Official JEP 505 page showing the 'Fifth Preview' status and the revision history of Structured Concurrency since JDK 19. Reprodução: openjdk.org.

This matters because, for those maintaining a Java service in production, "preview" usually means "I don't touch this yet." And that's still the case here: using StructuredTaskScope in JDK 25 requires compiling with javac --release 25 --enable-preview Main.java and running with java --enable-preview Main. Without these flags, the API won't even compile. The feature has matured since its incubation in JEP 428 (JDK 19) and has gone through six JDKs so far, but it still hasn't become a stable contract that survives a version upgrade without code review.

Simplify concurrent programming by introducing an API for structured concurrency.

JEP 505, OpenJDK

From ExecutorService chaos to structured scope

The problem the JEP describes is familiar to anyone who has written a handle() method that fires two I/O calls in parallel with ExecutorService. In the current pattern, each subtask becomes an independent Future, and the main code calls .get() on each to wait for the result. The JEP lists three ways this can go wrong in production.

  • If findUser() fails, the method throws an exception when calling user.get(), but fetchOrder() keeps running on its own thread: a thread leak.
  • If the thread executing handle() is interrupted, the interruption doesn't propagate to the subtasks, which keep running even after handle() has already failed.
  • If findUser() takes a long time and fetchOrder() fails first, handle() unnecessarily waits for findUser() to finish, instead of canceling it as soon as the fetchOrder() error appears.

The JEP's central point is that this kind of bug arises because ExecutorService and Future allow unrestricted concurrency: any thread can submit work to an executor, and any other thread, unrelated to the first, can call .get() to wait for the result. There is no parent-child structure between tasks, so the runtime has no way to automatically propagate cancellation or errors. Fixing this manually, with try/finally calling cancel() on the other futures, works, but it's easy to get wrong and hard to review in code review.

What changes in the fifth preview

The core API remains StructuredTaskScope, in the java.util.concurrent package. But the way a scope is opened has changed: in previous previews (JDK 21 to 24), the scope was created from a public constructor, usually from a ready-made policy subclass. In JEP 505, public constructors are out, and static factory methods take their place.

java
public static <T> StructuredTaskScope<T, Void> open();
public static <T, R> StructuredTaskScope<T, R> open(
 Joiner<? super T, ? extends R> joiner);

The parameterless open() covers the common case: it waits for all subtasks to finish successfully, or fails as soon as one of them fails. For other completion policies, the preview introduces the concept of Joiner, passed to the one-parameter version of open(). This is where the biggest code difference lies between those who had already tried a previous preview of Structured Concurrency and those trying it now for the first time: the skeleton of the try-with-resources is similar, but the line that opens the scope changes.

java
Response handle() throws InterruptedException {
 try (var scope = StructuredTaskScope.open()) {
 Subtask<String> user = scope.fork(() -> findUser());
 Subtask<Integer> order = scope.fork(() -> fetchOrder());
 scope.join(); // propagates exceptions from the subtasks
 return new Response(user.get(), order.get());
 }
}

Notice that the return type of fork() is no longer Future, but Subtask, a change that had already happened in JEP 453 (JDK 21) and remains in place. The scope.join() call waits for all subtasks as a unit; only after it is it safe to call Subtask::get().

Cancellation and error propagation, in practice

The practical gain of the structured model is that the lifecycle of child threads is bound to the lexical block of the try-with-resources. This resolves, by construction, the three problems that manual ExecutorService left open.

AspectManual ExecutorServiceStructuredTaskScope (JEP 505)
Cancellation on failureManual, via cancel() on each futureAutomatic: failure of one subtask cancels the siblings
Interruption propagationDoes not propagate to subtasksPropagates on exiting the scope
Thread lifetime scopeUndefined, threads can leakConfined to the try block
Observability in thread dumpThreads appear loose, unrelatedSubtasks appear as children of the scope
API stabilityStable since Java 5Preview, 5th round, still changes between versions

The close() method, implicitly called when exiting the try-with-resources, always waits for the subtask threads to finish, even if the scope has already been canceled. That's why the JEP is explicit on a point that directly affects anyone dealing with legacy code: subtasks need to respond to interruption quickly. If a subtask blocks on a call that isn't interruptible, it can delay the scope's closing indefinitely, and that's a real risk in old codebases, full of blocking I/O written before the era of virtual threads.

Why not switch in production yet (and why understanding it is already worthwhile)

The JEP is direct about its non-goals: it's not a goal to replace ExecutorService or Future, nor to create "the" definitive structured concurrency API for every Java program. The document itself leaves the door open for third-party libraries or future JDK versions to define other constructs. For those maintaining a service in production, this is a sign that StructuredTaskScope is complementary, not a mandatory replacement for what already works.

The practical recommendation, given this stage, is the usual one: don't adopt --enable-preview in production, because the way of opening the scope has already changed from a constructor to open() between previews, and JEP 525 signals it will change once more. It is worth, however, running the examples in a test environment and mapping out where, today, the code uses ExecutorService with manual try/finally for cancellation, because that's exactly the pattern that Structured Concurrency, once stabilized, should simplify.

The combination with virtual threads (JEP 444) is the reason the JEP treats this as a priority: each subtask runs by default on a virtual thread, which makes it feasible to dedicate one thread per I/O operation even in services with thousands of concurrent requests.

Source 1: JEP 505: Structured Concurrency (Fifth Preview/Final), OpenJDK (https://openjdk.org/jeps/505)

JEP 505: Structured Concurrency (Fifth Preview). Authors: Alan Bateman, Viktor Klang, and Ron Pressler. Status in the official document: Closed / Delivered, Release 25. Reviewed by Paul Sandoz. The document relates to JEP 499 (Fourth Preview) and already points to JEP 525 (Sixth Preview) as the next step, confirming that the API remains in preview stage in this JDK version.

JEP 505, OpenJDK

Translated from the Brazilian Portuguese original · Read the original