Java 26 Structured Concurrency: What's New in the Sixth Preview
The Problem with Traditional Concurrency
“I have a method that needs to call three different services. I parallelized them with CompletableFuture, but when one fails, the others keep running and I have no idea how to cancel them properly.”
This is a conversation I had with a developer last week. They had written concurrent code the “traditional” way—spawning threads, managing executors, chaining futures—and ended up with a mess of error handling and resource leaks.
Traditional Java concurrency has a fundamental problem: it treats concurrent subtasks as independent entities when they’re actually part of a single logical operation.
Consider this common pattern:
ExecutorService executor = Executors.newCachedThreadPool();
Future<String> userFuture = executor.submit(() -> fetchUser(userId));Future<List<Order>> ordersFuture = executor.submit(() -> fetchOrders(userId));
try { String user = userFuture.get(); List<Order> orders = ordersFuture.get(); return new UserProfile(user, orders);} catch (InterruptedException | ExecutionException e) { // What happens to the other future if one fails? // How do I cancel both properly? // What if fetchUser throws but fetchOrders is still running? throw new RuntimeException(e);}The code looks reasonable. But here’s what goes wrong:
+----------------------------------+| fetchUser() fetchOrders() || | | || v v || SUCCESS FAILS || | | || | Exception! || | | || Still running! <-- Problem: || | | || Resource leak Orphaned task |+----------------------------------+When fetchOrders() fails, fetchUser() keeps running. If fetchUser() fails, ordersFuture is orphaned. Canceling futures properly requires careful cleanup code that most developers skip.
Direct Answer
Structured Concurrency treats multiple concurrent subtasks as a single unit of work. All subtasks complete together, fail together, or get canceled together.
Java 26’s JEP 525 is the sixth preview of this API. It works with Virtual Threads (finalized in Java 21) to make concurrent code that’s easy to write, read, and debug.
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Future<String> user = scope.fork(() -> fetchUser(userId)); Future<List<Order>> orders = scope.fork(() -> fetchOrders(userId));
scope.join(); // Wait for both tasks scope.throwIfFailed(); // Handle errors
return new UserProfile(user.result(), orders.result());}The difference is stark. When fetchOrders() fails in the structured version:
- The scope automatically shuts down
fetchUser()gets canceled- Resources are cleaned up
- The exception propagates cleanly
No manual cleanup. No orphaned threads. No resource leaks.
Why This Matters Now
Virtual Threads changed the game. Before Java 21, creating a million threads was impractical. Now it’s trivial:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { // This is fine now - virtual threads are cheap IntStream.range(0, 1_000_000).forEach(i -> { executor.submit(() -> processRequest(i)); });}But with millions of threads, you need structure. You can’t manage thread lifecycles manually at that scale. Structured Concurrency provides the programming model that Virtual Threads require.
Together, they enable:
- High-throughput applications without reactive programming complexity
- Simple, readable code that looks synchronous
- Reliable error handling and cancellation
- Clear debugging with preserved call hierarchies
How Structured Concurrency Works
The core concept is the StructuredTaskScope. It creates a bounded context where concurrent subtasks execute:
Main Thread │ ├── Create StructuredTaskScope │ ├── fork() ──────────────────┐ │ ├── Subtask 1 (Virtual Thread) │ └── Subtask 2 (Virtual Thread) │ ├── join() ── Wait for all subtasks │ ├── throwIfFailed() ── Handle errors │ ├── Process results │ └── Close scope (automatic via try-with-resources)The scope guarantees:
- Lifetime binding: Subtasks cannot outlive their scope
- Cancellation propagation: Canceling the scope cancels all subtasks
- Error containment: One failure can trigger controlled shutdown
- Resource cleanup: try-with-resources ensures proper cleanup
The Three Scope Policies
Java provides three built-in policies for handling subtask failures:
ShutdownOnFailure
Cancel all running subtasks if any subtask fails:
public UserProfile fetchProfile(Long userId) throws Exception { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Future<String> user = scope.fork(() -> fetchUser(userId)); Future<List<Order>> orders = scope.fork(() -> fetchOrders(userId)); Future<List<Payment>> payments = scope.fork(() -> fetchPayments(userId));
scope.join(); scope.throwIfFailed();
return new UserProfile( user.result(), orders.result(), payments.result() ); }}If fetchOrders() throws, fetchUser() and fetchPayments() get canceled immediately. You either get all results or an exception.
ShutdownOnSuccess
Complete as soon as any subtask succeeds:
public String fetchFromAnyMirror(Long fileId) throws Exception { try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) { for (String mirror : MIRRORS) { scope.fork(() -> fetchFromMirror(mirror, fileId)); }
scope.join(); return scope.result(); }}This is useful for redundant services. The first successful response wins; others get canceled.
Custom Policy
For complex scenarios, extend StructuredTaskScope:
class BoundedScope<T> extends StructuredTaskScope<T> { private final int maxConcurrency; private int active = 0;
@Override protected void handleComplete(Future<T> future) { // Custom logic for handling completion synchronized (this) { active--; notifyAll(); } }
public void forkWithLimit(Callable<T> task) throws InterruptedException { synchronized (this) { while (active >= maxConcurrency) { wait(); } active++; } super.fork(task); }}What Changed in the Sixth Preview
JEP 525 refines the API based on extensive community feedback. Key changes include:
- API Stabilization: The core API has stabilized after five preview iterations
- Improved Error Messages: Better diagnostics when scopes are misused
- Performance Optimizations: Better integration with the virtual thread scheduler
- Documentation Clarity: Clearer guidance on when to use each policy
The API hasn’t changed dramatically since earlier previews—the team is focused on polish and edge cases rather than major redesigns.
Practical Example: Building a Dashboard
Let me show you a real-world use case. I need to build a dashboard that aggregates data from multiple services:
public record Dashboard( UserInfo user, List<Notification> notifications, AccountSummary account, List<Recommendation> recommendations) {}
public Dashboard loadDashboard(Long userId) throws DashboardException { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { // All these calls run concurrently on virtual threads Future<UserInfo> user = scope.fork(() -> userService.getUser(userId)); Future<List<Notification>> notifications = scope.fork( () -> notificationService.getRecent(userId) ); Future<AccountSummary> account = scope.fork( () -> accountService.getSummary(userId) ); Future<List<Recommendation>> recommendations = scope.fork( () -> recommendationService.getForUser(userId) );
try { scope.join(); scope.throwIfFailed(); } catch (Exception e) { // Wrap in domain exception with context throw new DashboardException("Failed to load dashboard", e); }
return new Dashboard( user.result(), notifications.result(), account.result(), recommendations.result() ); }}Before Structured Concurrency, I would have needed:
- A custom executor configuration
- Manual future composition
- Careful error handling with multiple catch blocks
- Explicit resource cleanup
- Thread pool management
Now it’s one try-with-resources block. The code is clear about its intent: run these tasks concurrently, fail if any fail, clean up automatically.
Debugging Structured Concurrency
One benefit that surprised me: debugging is much clearer.
With traditional concurrency, stack traces show thread dumps that don’t match your code structure:
"pool-1-thread-3" #23 waiting on java.util.concurrent.ComtableFuture@7a811... at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(...) ... 20+ framework frames ... at com.example.UserService.fetchUser(UserService.java:45)With Structured Concurrency, the call hierarchy is preserved:
Exception in thread "main" java.util.concurrent.ExecutionException: ... at java.base/java.util.concurrent.StructuredTaskScope.throwIfFailed(...) at com.example.DashboardService.loadDashboard(DashboardService.java:28) at com.example.DashboardController.getDashboard(DashboardController.java:15) ... application frames ...Caused by: java.lang.RuntimeException: User service unavailable at com.example.UserService.fetchUser(UserService.java:47)You can trace the exception from the dashboard controller through the scope to the actual failing service. No jumping between thread dumps.
When to Use Structured Concurrency
Use it when:
- You have multiple independent operations that can run concurrently
- All operations contribute to a single result
- You want fail-fast behavior (cancel everything if one fails)
- You’re already using Virtual Threads
Don’t use it for:
- Fire-and-forget operations (use a regular executor)
- Unrelated background tasks
- Operations where partial results are acceptable
- Legacy code that can’t migrate to Java 21+
Migration Path
If you’re on Java 21+ with existing concurrent code:
public CompletableFuture<UserProfile> fetchProfile(Long userId) { CompletableFuture<String> userFuture = CompletableFuture.supplyAsync( () -> fetchUser(userId), executor ); CompletableFuture<List<Order>> ordersFuture = CompletableFuture.supplyAsync( () -> fetchOrders(userId), executor );
return userFuture.thenCombine(ordersFuture, UserProfile::new);}public UserProfile fetchProfile(Long userId) throws Exception { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Future<String> user = scope.fork(() -> fetchUser(userId)); Future<List<Order>> orders = scope.fork(() -> fetchOrders(userId));
scope.join(); scope.throwIfFailed();
return new UserProfile(user.result(), orders.result()); }}The tradeoff: synchronous-looking code instead of callback chains, but you need to handle checked exceptions.
Common Pitfalls
Pitfall #1: Blocking After fork()
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Future<String> result = scope.fork(() -> fetchUser(userId));
// WRONG: Blocking outside join() String user = result.get(); // Don't do this!
scope.join();}Always use join() first, then access results.
Pitfall #2: Using Traditional Threads Inside Scopes
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { // WRONG: Platform threads are expensive scope.fork(() -> { return Executors.newCachedThreadPool().submit(this::heavyWork).get(); });}Let Virtual Threads handle the concurrency. Don’t nest thread pools.
Pitfall #3: Ignoring Scope Closure
var scope = new StructuredTaskScope.ShutdownOnFailure();scope.fork(() -> task1());scope.fork(() -> task2());scope.join();// WRONG: Scope never closed, resources leakedAlways use try-with-resources.
Summary
Java 26’s JEP 525 brings Structured Concurrency closer to finalization. The key insight is treating concurrent subtasks as a cohesive unit rather than independent threads.
The combination of Virtual Threads and Structured Concurrency represents a fundamental shift in Java concurrency:
| Aspect | Traditional | Structured |
|---|---|---|
| Thread model | Expensive platform threads | Cheap virtual threads |
| Task management | Manual executors | Automatic scope lifecycle |
| Error handling | Complex try-catch chains | Automatic propagation |
| Cancellation | Manual, error-prone | Built-in, reliable |
| Debugging | Disconnected stack traces | Preserved call hierarchy |
If you’re building concurrent applications on Java 21+, start experimenting with Structured Concurrency now. The API is stable enough for production use with the preview flag, and the paradigm shift is worth the learning curve.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 JEP 525: Structured Concurrency (Sixth Preview)
- 👨💻 JEP 444: Virtual Threads
- 👨💻 Project Loom
- 👨💻 Java 26 Documentation
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments