<!DOCTYPE html><html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  </head>
  <body>
    <div class="moz-cite-prefix">On 12/10/2025 06:32, Jige Yu wrote:<br>
    </div>
    <blockquote type="cite" cite="mid:CAKD1RYtivJL8Fr8K-fXB3-PLYXbu-ZOuinW82z=K1OcS9MWVLw@mail.gmail.com">
      
      <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>
      </div>
    </blockquote>
    Just a general point on providing feedback: The feedback that we
    most value is feedback from people that have tried a feature or API
    in earnest. We regularly have people showing up here with
    alternative APIs proposals but it's never clear if they have the
    same goals, whether they've tried the feature, or have considered
    many use cases. This isn't a criticism of your proposal, it's just
    not clear if this is after trying the feature or not.<br>
    <br>
    <br>
    <blockquote type="cite" cite="mid:CAKD1RYtivJL8Fr8K-fXB3-PLYXbu-ZOuinW82z=K1OcS9MWVLw@mail.gmail.com">
      <div dir="ltr">
        <hr>
        <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>
        </ol>
      </div>
    </blockquote>
    The API has 5 instance methods and isn't too hard to get wrong. 
    Yes, it's an exception at runtime if someone joins before forking,
    or attempts to process the outcome before joining. With a few basic
    recipes/examples then it should be possible for someone to get
    started quickly. The issues dealing with cancellation and shutdown
    are difficult to get right and we hope this API will help to avoid
    several of issues with a relatively simple API.<br>
    <br>
    <br>
    <blockquote type="cite" cite="mid:CAKD1RYtivJL8Fr8K-fXB3-PLYXbu-ZOuinW82z=K1OcS9MWVLw@mail.gmail.com">
      <div dir="ltr">
        <ol start="1">
          <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>
        </ol>
      </div>
    </blockquote>
    We aren't happy with needing to wrap exceptions but it is no
    different to other concurrent APIs, e.g. Future. Countless hours
    have been spent on explorations to do better. All modelling of
    exceptions with type parameters lead to cumbersome usage, e.g. a
    type parameter for the exception thrown by subtasks and another type
    parameter for the exception thrown by join. If there were union
    types for exceptions or other changes to the language then we might
    do better.<br>
    <br>
    On anySuccessfulOrThrow, then it's like invokeAny and similar
    combinators in that it causes join to return a result from any
    subtasks or throw if all subtasks fail. It would be feasible to
    develop a Joiner that returns something like
    `record(Optional<T> result, Map<Subtask<T>,
    Throwable> exceptions)` where the map contains the subtasks that
    failed before the successful subtask. That would be harder to use
    than the simpler built-in and users always have the option of
    logging in the failed subtask.<br>
    <br>
    <br>
    <br>
    <blockquote type="cite" cite="mid:CAKD1RYtivJL8Fr8K-fXB3-PLYXbu-ZOuinW82z=K1OcS9MWVLw@mail.gmail.com">
      <div dir="ltr">
        <ol start="1">
          <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>
        </ol>
      </div>
    </blockquote>
    join always returns something. For allSuccessfulOrThrow it returns a
    stream of successful subtasks.<br>
    <br>
    I think your comment is really about cases where the subtasks return
    results of the same type vs. other cases where subtasks return
    results of different types. This is an area where we need feedback.
    To date, we've been assuming that the more common case is subtasks
    that return results of different types (arms and legs in your
    example). For these cases, it's more useful to keep a reference to
    the subtask so that you don't have to cast when handling the
    results. It may be that we don't have this right and the common case
    is homogeneous subtasks, in which case the default Joiner should be
    allSuccessfulOrThrow so you don't need to keep a reference to the
    subtasks.<br>
    <br>
    <br>
    <br>
    <blockquote type="cite" cite="mid:CAKD1RYtivJL8Fr8K-fXB3-PLYXbu-ZOuinW82z=K1OcS9MWVLw@mail.gmail.com">
      <div dir="ltr">
        <ol start="1">
          <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>
      </div>
    </blockquote>
    Developing a Joiner for more advanced/expert developers. We have
    several guidelines in the API docs, the more relevant here is that
    they aren't the place for business logic, and should be designed to
    be as general purpose as possible.<br>
    <br>
    <br>
    <br>
    <br>
    <blockquote type="cite" cite="mid:CAKD1RYtivJL8Fr8K-fXB3-PLYXbu-ZOuinW82z=K1OcS9MWVLw@mail.gmail.com">
      <div dir="ltr">
        <hr>
        <h2><b>Suggestions for a Simpler Model</b></h2>
        <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>
        </ol>
      </div>
    </blockquote>
    <br>
    We've been down the road of combinator or utility methods a number
    of times, and have decided not to propose that direction for this
    API. It's not too hard to what create a method that does what you
    want, e.g.<br>
    <br>
        <U, V, R> R callConcurrently(Callable<U> task1,
    Callable<V> task2, BiFunction<U, V, R> combine) {<br>
            try (var scope = StructuredTaskScope.open()) {<br>
                Supplier<U> subtask1 = scope.fork(task1);<br>
                Supplier<V> subtask2 = scope.fork(task2);<br>
                scope.join();<br>
                return combine.apply(subtask1.get(), subtask2.get());<br>
            }<br>
        }<br>
    <br>
    (there's a more general form of the example presented in the JEP),<br>
    <br>
    <br>
    <blockquote type="cite" cite="mid:CAKD1RYtivJL8Fr8K-fXB3-PLYXbu-ZOuinW82z=K1OcS9MWVLw@mail.gmail.com">
      <div dir="ltr">
        <ol start="1">
          <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>
        </ol>
      </div>
    </blockquote>
    <br>
    There are many use cases. Joiner defines a small set of static
    factory for built-ins that we hope will cover most usages,
    equivalent to the built-ins defined by Gatherers. The
    anySuccessfulOrThrow (which is "race" in some Scala libraries) fits
    in well. <br>
    <br>
    We do want to bring mapConcurrent (or a successor) into the
    structured fold but don't have a good proposal at this time.<br>
    <br>
    <br>
    <br>
    <br>
    <blockquote type="cite" cite="mid:CAKD1RYtivJL8Fr8K-fXB3-PLYXbu-ZOuinW82z=K1OcS9MWVLw@mail.gmail.com">
      <div dir="ltr">
        <ol start="1">
          <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>
      </div>
    </blockquote>
    STS is intended to usable by average developers. Implementing Joiner
    is more advanced/expert.  Early exploration did propose additions to
    ExecutorService, including a variant of inokveAll that short
    circuited when a task failed, but just hides everything about
    structured concurrency.<br>
    <br>
    -Alan<br>
    <br>
    <br>
  </body>
</html>