JDK 28 finalizes structured concurrency after seven preview rounds since JDK 19
JEP 543 takes StructuredTaskScope out of preview mode in JDK 28. Those already on JDK 27 just need to recompile without the flag; those coming from older versions need to adjust exception handling.

JEP 543 has been integrated into the JDK 28 source code, according to InfoQ, closing out the longest preview cycle any Project Loom feature has ever had. StructuredTaskScope, an API that lives in java.util.concurrent, reaches final status with no signature changes from the seventh preview, released in JDK 27. JDK 28 has general availability planned for March 2027, but the API's finalization is already on the mainline.
The journey began as an incubator API in JEP 428 (JDK 19) and JEP 437 (JDK 20), and continued through seven preview rounds: JEP 453 (JDK 21), JEP 462 (JDK 22), JEP 480 (JDK 23), JEP 499 (JDK 24), JEP 505 (JDK 25), JEP 525 (JDK 26), and JEP 533 (JDK 27). Across all versions, the core goal never changed: treat a group of subtasks running on different threads as a single unit of work, so that one failure cancels the others and the parent scope never returns before all child threads finish.
The problem the API solves
The ExecutorService/Future pair doesn't enforce this kind of boundary. A method can return while subtasks keep running in the background, and a failure only surfaces when someone calls Future.get(). This creates thread leaks and concurrency bugs that are hard to reproduce, exactly the kind of problem that motivated Project Loom from the start.
With StructuredTaskScope, the pattern becomes explicit: a scope is opened, subtasks are forked, the result is awaited, and the scope closes, in that order, always within the same block of code.
Response handle() throws ExecutionException, InterruptedException {
try (var scope = StructuredTaskScope.open()) {
Subtask<String> user = scope.fork(() -> findUser());
Subtask<Integer> order = scope.fork(() -> fetchOrder());
scope.join(); // throws ExecutionException if any subtask fails
return new Response(user.get(), order.get());
}
}Each fork() starts a virtual thread by default. join() is the only point where the owner thread waits for the result, and close(), at the end of the try-with-resources block, cancels any pending subtask and waits for the threads to finish. The API is deliberately strict: only the thread that opened the scope can call fork() and join(), and incorrect structural use, such as closing scopes out of order, fails fast with StructureViolationException instead of silently leaking threads.
Completion policies via Joiner
The Joiner<T, R, R_X> interface defines what happens when subtasks finish. Joiner.awaitAllSuccessfulOrThrow() is the default policy in the example above. Joiner.allSuccessfulOrThrow(), meanwhile, returns a List<T> directly from join() for homogeneous subtasks, Joiner.anySuccessfulOrThrow() returns the first successful result and cancels the rest, and Joiner.allUntil(Predicate) cancels the scope as soon as a predicate is satisfied.
The third type parameter, R_X, shows its usefulness in custom policies. A joiner that waits for a quorum of reads from replicas, for example, can expose its own checked exception instead of wrapping everything in ExecutionException:
class QuorumJoiner<T> implements Joiner<T, List<T>, QuorumException> {
private final int quorum;
private final Queue<T> results = new ConcurrentLinkedQueue<>();
QuorumJoiner(int quorum) { this.quorum = quorum; }
@Override
public boolean onComplete(Subtask<T> subtask) {
if (subtask.state() == Subtask.State.SUCCESS) {
results.add(subtask.get());
return results.size() >= quorum; // true cancels the remaining reads
}
return false;
}
public List<T> result() throws QuorumException {
if (results.size() < quorum) {
throw new QuorumException(results.size(), quorum);
}
return List.copyOf(results).subList(0, quorum);
}
}Because the joiner declares QuorumException as its R_X, join() throws that exception directly, without the generic wrapper. This gives infrastructure APIs, such as distributed database clients, a way to propagate domain errors without losing structured semantics.
For those already on JDK 27: just recompile
For teams that migrated to JDK 27 and already use StructuredTaskScope with the preview flag, finalization is a build change, not a code change. The commit that closes out JEP 543 only removes the preview annotations and doesn't change any method signature. In practice:
- Remove
--enable-previewfromjavac, test runners, and run scripts. - Recompile the project on a JDK 28 early-access build.
- No line of code changes.
For those coming from older versions: watch the exceptions
The scenario changes for those still on JDK 26 or earlier. JEP 533, which introduced the third type parameter R_X in JDK 27, replaced the preview-specific FailedException with ExecutionException, the same wrapper Future.get() has used for years. Custom joiners also need to declare the new type parameter in their signature.
Three additional changes break code written against previews before JDK 27:
StructuredTaskScope.TimeoutExceptionwas removed; timeouts now arrive as aCancelledByTimeoutExceptioncause inside the joiner's exception.Joiner.awaitAll()no longer exists.- The
onTimeout()callback was replaced by the abstracttimeout()method.
Timeout and deadline configuration, on the other hand, remains the same as in earlier versions: a UnaryOperator<Configuration> passed to open() sets the scope's name, a custom ThreadFactory, or a deadline that cancels the scope if it expires before or during join().
try (var scope = StructuredTaskScope.open(
cf -> cf.withTimeout(Duration.ofSeconds(2)).withName("profile-page"))) {
scope.join();
} catch (ExecutionException e) {
return switch (e.getCause()) {
case CancelledByTimeoutException _ -> handleTimeout();
case IOException ioe -> handleIo(ioe);
default -> throw e;
};
}What remains open
One detail was deliberately left unanswered: when several subtasks fail at the same time, the API doesn't guarantee which failure join() will report. According to InfoQ, Viktor Klang raised the question during code review, and Alan Bateman, the JEP's owner at Oracle, rewrote the Javadoc to promise only that the exception will come from some subtask that failed, without specifying which one.
Subtasks inherit ScopedValue (JEP 506) bindings and appear nested under the parent task in JSON thread dumps, which helps with debugging complex concurrent pipelines.
The effect on libraries and frameworks
The most significant change in the medium term is for those who maintain libraries and frameworks: it's now possible to build on structured concurrency without forcing preview flags on end users. The velo/structured-concurrency project, cited by InfoQ, already backports the JEP 543 API to JDK 25 and offers migration recipes for when the team finally moves to a version with the native API.
For those deciding on a stack today, the practical takeaway is direct: teams in production on JDK 25 or 26 that adopted StructuredTaskScope via preview have a concrete migration checklist ahead of them, while those who waited for JDK 27 or 28 avoid most of the friction.
Translated from the Brazilian Portuguese original · Read the original
PS5LM runs GGUF models via llama.cpp inside a jailbroken PS5
An open-source project gets llama.cpp running on the console's GPU and exposes the loaded model through an API compatible with OpenAI and Anthropic, accessible to any device on the local network.





