BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage News JEP 533 Tightens Exception Handling in Java's Structured Concurrency for JDK 27

JEP 533 Tightens Exception Handling in Java's Structured Concurrency for JDK 27

Listen to this article -  0:00

JEP 533, Structured Concurrency (Seventh Preview), has been elevated to integrated status for JDK 27. The iteration continues the steady refinement that has shaped the API since its first incubation in JDK 19 and subsequent previews beginning in JDK 21. Most of the attention this round goes to how exceptions flow out of a scope.

Structured concurrency, exposed through java.util.concurrent.StructuredTaskScope, together with the Joiner abstraction, treats groups of related subtasks as a single unit of work. It addresses three concerns that ad-hoc thread management leaves unsolved: confining subtask lifetimes to the parent scope, reliably propagating cancellation, and surfacing thread hierarchies in observability tools. Preview 6 (JDK 26) added the onTimeout() callback and adjusted allSuccessfulOrThrow() to return a List<T>. Preview 7 keeps that direction and focuses on exception ergonomics and type safety. JEP 533 frames the iteration as a focused refinement: the StructuredTaskScope and Joiner interfaces gain a third type parameter "for the type of the exception that the join() method of StructuredTaskScope can throw," and a new static open method "implements the default join policy and uses a given UnaryOperator to produce the StructuredTaskScope configuration."

The headline change is the new exception type thrown by join() for the three standard joiners. In recent previews, Joiner.allSuccessfulOrThrow(), anySuccessfulOrThrow(), and awaitAllSuccessfulOrThrow() raised the preview-specific FailedException when a subtask failed. In Preview 7, those joiners now throw ExecutionException, the same wrapper that has long signalled subtask failure in Future.get(). The cause is preserved on getCause(), so the familiar catch-then-switch pattern carries straight over:

try (var scope = StructuredTaskScope.open()) {
    Subtask<String> user   = scope.fork(() -> findUser(userId));
    Subtask<List<Order>> o = scope.fork(() -> fetchOrders(userId));
    scope.join();
    return new Response(user.get(), o.get());
} catch (ExecutionException e) {
    switch (e.getCause()) {
        case IOException ioe       -> handleIo(ioe);
        case TimeoutException te   -> handleTimeout(te);
        default                    -> throw e;
    }
}

The change reduces the conceptual gap between classic concurrency code and structured scopes. Teams already catching FailedException in earlier previews will need to update those catches to ExecutionException when moving to JDK 27.

The second change is structural. StructuredTaskScope and the Joiner interface now carry a third type parameter, R_X, representing the exception type that join() can throw. The previous signature was Joiner<T, R>; it becomes Joiner<T, R, R_X>. The R_X name follows the JEP’s convention for distinguishing the exception type from the result and joiner-return types in documentation; the compiler treats it like any other type parameter. For application code that uses the supplied joiners through open(), the compiler infers everything, and the source looks the same. For library authors writing custom joiners, the throws clause becomes part of the type rather than something the implementation declares separately. That makes signatures more honest and gives callers a precise checked-exception contract on join().

The third change adds a new open overload that pairs the default join policy, the behaviour of the zero-argument open(), which waits for all subtasks to succeed or any to fail, with a configuration operator, accepting a UnaryOperator over the scope's Configuration:

try (var scope = StructuredTaskScope.open(
        cfg -> cfg.withTimeout(Duration.ofSeconds(2)).withName("checkout"))) {
    scope.fork(() -> fetchCart(userId));
    scope.fork(() -> fetchProfile(userId));
    scope.join();
}

Until now, applying a timeout, a name, or a custom thread factory to the default fail-fast policy required passing a Joiner alongside the operator. The new factory removes that ceremony. The overload accepts a UnaryOperator<Configuration>, mirroring the tighter typing introduced in Preview 6.

The structural guarantees remain unchanged: subtasks inherit ScopedValue bindings (JEP 506), the JSON thread dump format continues to expose scope hierarchies for tools, and StructureViolationException still fires when a scope is used outside try-with-resources or forked from a non-owner thread.

Preview 7 is not a redesign. The shape of the API set in Preview 5 is holding, and the changes in this round are confined to ergonomics and typing rather than structure. For teams tracking the API, the narrowing scope of each preview is a reasonable indicator that the design is converging. Structured concurrency has moved through two incubator rounds and several previews. The API appears to be converging, though JEP 533 does not commit to a finalization timeline.

Developers can experiment with the proposal in JDK 27 early-access builds by enabling preview features with --enable-preview. Feedback through the OpenJDK mailing lists continues to shape the API ahead of finalization.

About the Author

Rate this Article

Adoption
Style

BT