Well said. 

> On Oct 12, 2025, at 12:33 AM, Jige Yu <[email protected]> wrote:
> 
> 
> 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
> 
> 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.
> 
> 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.
> 
> 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.
> 
> 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.
> 
> 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)
> );
> 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.
> 
> 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.

Reply via email to