Feedback on Structured Concurrency (JEP 525, 6th Preview)
Jige Yu
yujige at gmail.com
Sun Oct 12 05:32:33 UTC 2025
Hi Project Loom.First and foremost, I want to express my gratitude for the
effort that has gone into structured concurrency. API design in this space
is notoriously difficult, and this feedback is offered with the greatest
respect for the team's work and in the spirit of collaborative refinement.
My perspective is that of a developer looking to use Structured Concurrency
for common, IO-intensive fan-out operations. My focus is to replace
everyday async callback hell, or reactive chains with something simpler and
more readable.
It will lack depth in the highly specialized concurrent programming area.
And I acknowledge this viewpoint may bias my feedback.
------------------------------
*High-Level Impression*
>From this perspective, the current API feels imperative and more complex
for the common intended use cases than necessary. It introduces significant
cognitive load through its stateful nature and manual lifecycle management.
------------------------------
*Specific Points of Concern*
1.
*Stateful and Imperative API:* The API imposes quite some "don't do this
at time X" rules. Attempting to fork() after join() leads to a runtime
error; forgetting to call join() is another error; and the imperative
fork/join sequence is more cumbersome than a declarative approach would
be. None of these are unmanageable though.
2.
*Challenging Exception Handling:* The exception handling model is tricky:
-
*Loss of Checked Exception Compile-Time Safety:* FailedException is
effectively an unchecked wrapper that erases checked exception
information
at compile time. Migrating from sequential, structured code to concurrent
code now means losing valuable compiler guarantees.
-
*No Help For Exception Handling: *For code that wants to catch and
handle these exceptions, it's the same story of using *instanceof* on
the getCause(), again, losing all compile-time safety that was
available in
equivalent sequential code.
-
*Burdensome InterruptedException Handling:* The requirement for the
caller to handle or propagate InterruptedException from join() will
add room for error as handling InterruptedException is easy to get wrong:
one can forget to call currentThread().interrupt(). Or, if the caller
decides to declare *throws* *InterruptedException*, the signature
propagation becomes viral.
-
*Default Exception Swallowing:* The AnySuccessOrThrow policy
*swallows all
exceptions* by default, including critical ones like
NullPointerException, IllegalArgumentException, or even an Error.
This makes it dangerously easy to mask bugs that should be
highly visible.
There is no straightforward mechanism to inspect these suppressed
exceptions or fail on specific, unexpected types.
3.
*Conflated API Semantics:* The StructuredTaskScope API unifies two very
different concurrency patterns—"gather all" (allSuccessfulOrThrow) and
"race to first success" (anySuccessfulResultOrThrow)—under a single
class but with different interaction models for the same method.
-
In the *"gather all"* pattern (allSuccessfulOrThrow), join() returns
void. The callsite should use subtask.get() to retrieve results.
-
In the *"race"* pattern (anySuccessfulResultOrThrow), join() returns
the result (R) of the first successful subtask directly. The
developer should *not* call get() on individual subtasks. Having the
join()+subtask.get() method spec'ed conditionally (which method to
use and how depends on the actual policy) feels like a minor violation of
LSP and is a source of confusion. It may be an indication of premature
abstraction.
4.
*Overly Complex Customization:* The StructuredTaskScope.Policy API,
while powerful, feels like a potential footgun. The powerful lifecycle
callback methods like onFork(), onComplete(), onTimeout() may lower the
barrier to creating intricate, framework-like abstractions that are
difficult to reason about and debug.
------------------------------
*Suggestions for a Simpler Model*
My preference is that the API for the most common use cases should be
more *declarative
and functional*.
1.
*Simplify the "Gather All" Pattern:* The primary "fan-out and gather"
use case could be captured in a simple, high-level construct. An average
user shouldn't need to learn the wide API surface of StructuredTaskScope +
Joiner + the lifecycles. For example:
Java
// Ideal API for the 80% use case
Robot robot = Concurrently.call(
() -> fetchArm(),
() -> fetchLeg(),
(arm, leg) -> new Robot(arm, leg)
);
2.
*Separate Race Semantics into Composable Operations:* The "race" pattern
feels like a distinct use case that could be implemented more naturally
using composable, functional APIs like Stream gatherers, rather than
requiring a specialized API at all. For example, if mapConcurrent()
fully embraced structured concurrency, guaranteeing fail-fast and
happens-before, a recoverable race could be written explicitly:
Java
// Pseudo-code for a recoverable race using a stream gatherer
<T> T race(Collection<Callable<T>> tasks, int maxConcurrency) {
var exceptions = new ConcurrentLinkedQueue<RpcException>();
return tasks.stream()
.gather(mapConcurrent(maxConcurrency, task -> {
try {
return task.call();
} catch (RpcException e) {
if (isRecoverable(e)) { // Selectively recover
exceptions.add(e);
return null; // Suppress and continue
}
throw new RuntimeException(e); // Fail fast on
non-recoverable
}
}))
.filter(Objects::nonNull)
.findFirst() // Short-circuiting and cancellation
.orElseThrow(() -> new AggregateException(exceptions));
}
While this is slightly more verbose than the JEP example, it's familiar
Stream semantics that people have already learned, and it offers explicit
control over which exceptions are recoverable versus fatal. The boilerplate
for exception aggregation could easily be wrapped in a helper method.
3.
*Reserve Complexity for Complex Cases:* The low-level StructuredTaskScope
and its policy mechanism are powerful tools. However, they should be
positioned as the "expert-level" API for building custom frameworks. Or
perhaps just keep them in the traditional ExecutorService API. The everyday
developer experience should be centered around simpler, declarative
constructs that cover the most frequent needs.
------------------------------
I realize my perspective is heavily biased towards the 'everyday' use case
and I may not realize or appreciate the full scope of problems the JEP aims
to solve. And I used a lot of "feels". ;->
Anyhow, please forgive ignorance and disregard any points that don't align
with the project's broader vision.
Thank you again for your dedication to moving Java forward.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/loom-dev/attachments/20251011/9bfc466b/attachment-0001.htm>
More information about the loom-dev
mailing list