<div dir="ltr"><h3><span style="font-size:small;font-weight:normal">Hi Project Loom.</span></h3><h3><span style="font-size:small;font-weight:normal">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.</span></h3><p>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 <font face="monospace">async callback hell,</font> or reactive chains with something simpler and more readable.</p><p>It will lack depth in the highly specialized concurrent programming area. And I acknowledge this viewpoint may bias my feedback.</p><hr><p></p><h2><b>High-Level Impression</b></h2><p></p><p>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.</p><hr><p></p><h2><b>Specific Points of Concern</b></h2><p></p><ol start="1"><li><p><b>Stateful and Imperative API:</b> The API imposes quite some "don't do this at time X" rules. Attempting to <code>fork()</code> after <code>join()</code> leads to a runtime error; forgetting to call join() is another error; and the imperative <code>fork</code>/<code>join</code> sequence is more cumbersome than a declarative approach would be. None of these are unmanageable though.</p></li><li><p><b>Challenging Exception Handling:</b> The exception handling model is tricky:</p><ul><li><p><b>Loss of Checked Exception Compile-Time Safety:</b> <code>FailedException</code> 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. </p></li><li><p><b>No Help For Exception Handling: </b>For code that wants to catch and handle these exceptions, it's the same story of using <i>instanceof</i> on the getCause(), again, losing all compile-time safety that was available in equivalent sequential code.</p></li><li><p><b>Burdensome <code>InterruptedException</code> Handling:</b> The requirement for the caller to handle or propagate <code>InterruptedException</code> from <code>join()</code> 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 <i>throws</i> <span style="font-family:monospace"><i>InterruptedException</i></span>, the signature propagation becomes viral.</p></li><li><p><b>Default Exception Swallowing:</b> The <code>AnySuccessOrThrow</code> policy <b>swallows all exceptions</b> by default, including critical ones like <code>NullPointerException</code>, <code>IllegalArgumentException</code>, or even an <code>Error</code>. 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.</p></li></ul></li><li><p><b>Conflated API Semantics:</b> The <code>StructuredTaskScope</code> API unifies two very different concurrency patterns—"gather all" (<font face="monospace">allSuccessfulOrThrow</font>) and "race to first success" (<code>anySuccessfulResultOrThrow</code>)—under a single class but with different interaction models for the same method.</p><ul><li><p>In the <b>"gather all"</b> pattern (<code>allSuccessfulOrThrow</code>), <code>join()</code> returns <code>void</code>. The callsite should use <code>subtask.get()</code>  to retrieve results.</p></li><li><p>In the <b>"race"</b> pattern (<code>anySuccessfulResultOrThrow</code>), <code>join()</code> returns the result (<code>R</code>) of the first successful subtask directly. The developer should <i>not</i> call <code>get()</code> on individual subtasks.
Having the <code>join()+subtask.get()</code> 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.</p></li></ul></li><li><p><b>Overly Complex Customization:</b> The <code>StructuredTaskScope.Policy</code> 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.</p></li></ol><hr><p></p><h2><b>Suggestions for a Simpler Model</b></h2><p></p><p>My preference is that the API for the most common use cases should be more <b>declarative and functional</b>.</p><ol start="1"><li><p><b>Simplify the "Gather All" Pattern:</b> 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:</p><span class="gmail-"><span class="gmail-ng-tns-c3565927614-183 gmail-ng-star-inserted"><div class="gmail-code-block gmail-ng-tns-c3565927614-183 gmail-ng-animate-disabled gmail-ng-trigger gmail-ng-trigger-codeBlockRevealAnimation"><div class="gmail-code-block-decoration gmail-header-formatted gmail-gds-title-s gmail-ng-tns-c3565927614-183 gmail-ng-star-inserted"><span class="gmail-ng-tns-c3565927614-183">Java</span><div class="gmail-buttons gmail-ng-tns-c3565927614-183 gmail-ng-star-inserted"><button aria-label="Copy code" class="gmail-mdc-icon-button gmail-mat-mdc-icon-button gmail-mat-mdc-button-base gmail-mat-mdc-tooltip-trigger gmail-copy-button gmail-ng-tns-c3565927614-183 gmail-mat-unthemed gmail-_mat-animation-noopable gmail-ng-star-inserted"><span class="gmail-mat-mdc-button-persistent-ripple gmail-mdc-icon-button__ripple"></span><span role="img" class="gmail-mat-icon gmail-notranslate gmail-google-symbols gmail-mat-ligature-font gmail-mat-icon-no-color" aria-hidden="true"></span><span class="gmail-mat-focus-indicator"></span><span class="gmail-mat-mdc-button-touch-target"></span></button></div></div><div class="gmail-formatted-code-block-internal-container gmail-ng-tns-c3565927614-183"><div class="gmail-animated-opacity gmail-ng-tns-c3565927614-183"><pre class="gmail-ng-tns-c3565927614-183"><code role="text" class="gmail-code-container gmail-formatted gmail-ng-tns-c3565927614-183"><span class="gmail-hljs-comment">// Ideal API for the 80% use case</span>
Robot robot = Concurrently.call(
    () -> fetchArm(),
    () -> fetchLeg(),
    (arm, leg) -> <span class="gmail-hljs-keyword">new</span> Robot(arm, leg)
);
</code></pre></div></div></div></span></span></li><li><p><b>Separate Race Semantics into Composable Operations:</b> 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 <code>mapConcurrent()</code> fully embraced structured concurrency, guaranteeing fail-fast and happens-before, a recoverable race could be written explicitly:</p><span class="gmail-"><span class="gmail-ng-tns-c3565927614-184 gmail-ng-star-inserted"><div class="gmail-code-block gmail-ng-tns-c3565927614-184 gmail-ng-animate-disabled gmail-ng-trigger gmail-ng-trigger-codeBlockRevealAnimation"><div class="gmail-code-block-decoration gmail-header-formatted gmail-gds-title-s gmail-ng-tns-c3565927614-184 gmail-ng-star-inserted"><span class="gmail-ng-tns-c3565927614-184">Java</span><div class="gmail-buttons gmail-ng-tns-c3565927614-184 gmail-ng-star-inserted"><button aria-label="Copy code" class="gmail-mdc-icon-button gmail-mat-mdc-icon-button gmail-mat-mdc-button-base gmail-mat-mdc-tooltip-trigger gmail-copy-button gmail-ng-tns-c3565927614-184 gmail-mat-unthemed gmail-_mat-animation-noopable gmail-ng-star-inserted"><span class="gmail-mat-mdc-button-persistent-ripple gmail-mdc-icon-button__ripple"></span><span role="img" class="gmail-mat-icon gmail-notranslate gmail-google-symbols gmail-mat-ligature-font gmail-mat-icon-no-color" aria-hidden="true"></span><span class="gmail-mat-focus-indicator"></span><span class="gmail-mat-mdc-button-touch-target"></span></button></div></div><div class="gmail-formatted-code-block-internal-container gmail-ng-tns-c3565927614-184"><div class="gmail-animated-opacity gmail-ng-tns-c3565927614-184"><pre class="gmail-ng-tns-c3565927614-184"><code role="text" class="gmail-code-container gmail-formatted gmail-ng-tns-c3565927614-184"><span class="gmail-hljs-comment">// Pseudo-code for a recoverable race using a stream gatherer</span>
<T> <span class="gmail-hljs-function">T <span class="gmail-hljs-title">race</span><span class="gmail-hljs-params">(Collection<Callable<T>> tasks, <span class="gmail-hljs-keyword">int</span> maxConcurrency)</span> </span>{
    <span class="gmail-hljs-keyword">var</span> exceptions = <span class="gmail-hljs-keyword">new</span> ConcurrentLinkedQueue<RpcException>();
    <span class="gmail-hljs-keyword">return</span> tasks.stream()
        .gather(mapConcurrent(maxConcurrency, task -> {
            <span class="gmail-hljs-keyword">try</span> {
                <span class="gmail-hljs-keyword">return</span> task.call();
            } <span class="gmail-hljs-keyword">catch</span> (RpcException e) {
                <span class="gmail-hljs-keyword">if</span> (isRecoverable(e)) { <span class="gmail-hljs-comment">// Selectively recover</span>
                    exceptions.add(e);
                    <span class="gmail-hljs-keyword">return</span> <span class="gmail-hljs-keyword">null</span>; <span class="gmail-hljs-comment">// Suppress and continue</span>
                }
                <span class="gmail-hljs-keyword">throw</span> <span class="gmail-hljs-keyword">new</span> RuntimeException(e); <span class="gmail-hljs-comment">// Fail fast on non-recoverable</span>
            }
        }))
        .filter(Objects::nonNull)
        .findFirst() <span class="gmail-hljs-comment">// Short-circuiting and cancellation</span>
        .orElseThrow(() -> <span class="gmail-hljs-keyword">new</span> AggregateException(exceptions));
}
</code></pre></div></div></div></span></span><p>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.</p></li><li><p><b>Reserve Complexity for Complex Cases:</b> The low-level <code>StructuredTaskScope</code> 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.</p></li></ol><hr><p>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". ;-></p><p>Anyhow, please forgive ignorance and disregard any points that don't align with the project's broader vision.</p><p>Thank you again for your dedication to moving Java forward.</p></div>