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