Core Java

Structured Concurrency: What Changes for the Try/Catch Generation

A real CompletableFuture chain, rewritten into StructuredTaskScope, plus the JDBC cancellation gap nobody’s fixed yet.

Java developers who learned concurrency on CompletableFuture chains and manual thread pools are about to get a genuinely different tool. Structured Concurrency changes how failure, cancellation, and thread lifetimes work, not just the syntax. But before getting to the migration, it’s worth correcting a detail that’s spreading faster than the feature itself: Structured Concurrency is not finalized. Not yet.

Where Things Actually Stand

As of JDK 27, due in September 2026, Structured Concurrency is in its seventh preview, delivered as JEP 533. It has previewed, in some form, in every release since JDK 19 in 2022, and JEP 533’s own text does not commit to a finalization timeline. The API is converging, the changes each round are smaller than the last, but “converging” and “final” are different words for a reason. If you’re planning production code around this feature today, it still needs --enable-preview at compile time and runtime, and the API surface can still shift under you between JDK versions.

What is finalized, and worth using without hesitation, is Scoped Values, delivered as JEP 506 in JDK 25. Scoped Values are the replacement for ThreadLocal in virtual-thread code, and they integrate directly with structured concurrency scopes, subtasks automatically inherit the scoped value bindings of the parent. If you’re adopting one of these two features now, Scoped Values is the one you can ship without a preview flag.

The Old Way: A CompletableFuture Chain

Here’s a common pattern: fetch a user record and that user’s orders concurrently, combine them into a response, and give up if either call takes too long.

CompletableFuture<User> userFuture = CompletableFuture
    .supplyAsync(() -> findUser(userId), executor)
    .orTimeout(2, TimeUnit.SECONDS);

CompletableFuture<List<Order>> ordersFuture = CompletableFuture
    .supplyAsync(() -> fetchOrders(userId), executor)
    .orTimeout(2, TimeUnit.SECONDS);

CompletableFuture<Response> combined = userFuture
    .thenCombine(ordersFuture, Response::new)
    .exceptionally(ex -> {
        userFuture.cancel(true);
        ordersFuture.cancel(true);
        throw new CompletionException(ex);
    });

Response response = combined.join();

This works, but every line is doing manual bookkeeping the language should be handling. The .exceptionally() block has to manually cancel the sibling future because thenCombine won’t do it for you, and even that cancellation is mostly cosmetic: CompletableFuture.cancel(true) marks the future as cancelled, it does not reliably interrupt the thread actually running findUser() unless you’ve wired that up yourself. A thread dump under load shows a flat pool of generically named threads with no indication which request they belong to. None of this is a bug in CompletableFuture, it’s simply an API built on unstructured concurrency, where tasks have no enforced relationship to each other.

The New Way: StructuredTaskScope

The same logic, expressed with the current preview API from JEP 505 and JEP 525:

Response handle(String userId) throws InterruptedException, ExecutionException {
    try (var scope = StructuredTaskScope.open(
            StructuredTaskScope.Joiner.awaitAllSuccessfulOrThrow(),
            cfg -> cfg.withTimeout(Duration.ofSeconds(2)))) {

        StructuredTaskScope.Subtask<User> user =
            scope.fork(() -> findUser(userId));
        StructuredTaskScope.Subtask<List<Order>> orders =
            scope.fork(() -> fetchOrders(userId));

        scope.join();

        return new Response(user.get(), orders.get());
    }
}

The cancellation logic that took a manual .exceptionally() block before is now the scope’s job. If either subtask fails, or the two-second timeout fires, join() throws, the try-with-resources block closes the scope, and the scope interrupts whichever subtask is still running before returning control to the caller. Nobody wrote that coordination by hand, it’s a property of the scope itself.

CompletableFuture conceptStructuredTaskScope equivalent
supplyAsync(fn, executor)scope.fork(() -> ...), runs on a virtual thread by default
.orTimeout(...) per futureOne timeout on the whole scope, via the configuration lambda
Manual .cancel(true) on siblingsAutomatic, scope cancels remaining subtasks on any failure
.thenCombine() / .join()scope.join(), then read each Subtask.get()
Flat, unlabeled thread pool in a dumpThreads grouped under their owning scope in the thread dump

Where It Still Falls Short: The JDBC Problem

The gap that matters most in practice: Structured Concurrency’s entire cancellation model depends on subtasks actually responding to interruption. The JEP’s own specification says it plainly, subtasks that block on methods that are not interruptible may delay the closing of a scope indefinitely, and the close() method will wait for them to finish no matter what. That’s exactly what happens with most JDBC drivers today.

A large share of production Java services fork a subtask to run a JDBC query. JDBC calls are blocking by design, and the underlying socket reads inside many drivers do not honor Thread.interrupt() the way NIO channels do. When a sibling subtask fails and the scope tries to cancel that still-running database call, the interrupt gets delivered, but the thread sitting inside a blocking socket read simply doesn’t notice. The scope’s close() call then blocks too, waiting for a subtask that has no intention of stopping until the underlying query finishes or the connection times out on its own. The clean, structured cancellation the whole feature is built around quietly stops working at exactly the boundary where most Java applications spend most of their I/O time.

This isn’t a defect in Structured Concurrency, and it isn’t really a defect in JDBC either, it’s a mismatch between a cooperative interruption model and drivers written decades before that model existed. It also compounds with a related, separate issue: some JDBC drivers still use synchronized blocks internally that pin virtual threads to their carrier during blocking calls, which is a scheduling problem rather than a cancellation one, but shows up in the same code paths and makes diagnosing the root cause harder.

Every JDK release Structured Concurrency has touched, and which preview round it shipped as. No release has finalized it yet.

What to actually do about it today

  • Adopt Scoped Values now, they’re finalized in JDK 25 and safe to use without a preview flag.
  • Gate any Structured Concurrency code behind --enable-preview, and expect the API to keep shifting slightly until it actually finalizes.
  • Audit which of your subtasks make blocking JDBC calls, and test what actually happens to scope shutdown time when one of those calls hangs.
  • Where possible, set aggressive query timeouts at the JDBC driver or connection-pool level, don’t rely on scope cancellation alone to bound a slow query.
  • Watch driver changelogs, several major JDBC drivers are actively working through virtual-thread and interruption compatibility issues; this gap is closing, not permanent.

What We Learned

Structured Concurrency replaces the manual bookkeeping of CompletableFuture chains, cancelling siblings by hand, chasing down which thread belongs to which request, with a scope that owns that coordination itself: fork subtasks, call join(), and let a single timeout or failure cancel everything else automatically. But the feature is still in its seventh preview as of JDK 27, with no committed finalization date, so treat “production-ready” claims about it with the same caution the JEP authors themselves are showing. And its cancellation model has a real, specification-acknowledged limit: it only works if the code being cancelled actually responds to interruption, which a great deal of JDBC traffic still doesn’t.

Scoped Values, by contrast, are finalized today and worth adopting regardless of how the rest of this story plays out. The try/catch generation isn’t being asked to abandon everything it knows, just to recognize that the biggest win here is structural correctness, not a syntax upgrade, and that the win still has a real edge where blocking I/O meets a cooperative cancellation model that assumes everyone plays along.

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button