<div dir="ltr">Understood your perspective, Peter. Thanks for the response!<div><br></div><div>I think my earlier comment could be interpreted the wrong way. </div><div><br></div><div>So if I may take it back to phrase it more accurately, this feedback is more like:</div><div><br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">A swiss army knife is what I need everyday. This jigsaw <i>can</i> cut for me, but the sharp edges and the unwieldiness are my concern.</blockquote></div><br><div class="gmail_quote gmail_quote_container"><div dir="ltr" class="gmail_attr">On Sun, Oct 12, 2025 at 6:47 PM Peter Eastham <<a href="mailto:petereastham@gmail.com">petereastham@gmail.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div dir="ltr">> 

I think my type of "yeah, but my use case is so simple, I don't need this powerful tool" feedback, raising a use case that exceeds the capability of the functional API, yet is still considered common, would have been convincing?<br><br>There are always going to be sharp edges and missing features to any API. Mentioning them is useful, the real world examples help to put the context around <i>how bad</i> those aspects are. Since we have a mature preview API, it enables people to provide more than thoughts.<br><br>If we're going to focus on my comment, "I think your best next step is to either create or find and contribute to some OSS Library that wraps STS", I'll call out that the important part was "next step". I'm sorry if this came across as "Don't comment without a real world example".<br>-Peter</div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Sun, Oct 12, 2025 at 7:06 PM Jige Yu <<a href="mailto:yujige@gmail.com" target="_blank">yujige@gmail.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div dir="ltr">Yeah, I understand by not really having used the STS api seriously, I must have limited my understanding of it in some ways.<div><br></div><div>And I certainly don't claim that I know all its power and potential. Thus I'm sending the email to validate my overly-simplistic observation: a much simpler functional API would have sufficed.</div><div><br></div><div>If I were to propose a new functionality, then I totally should have tried using the STS API and see why it couldn't solve my need.</div><div><br></div><div>But here I'm not having a new use case or asking for a new feature.</div><div><br></div><div>I'm simply saying that for all the use cases I can visualize, I only need a very limited subset of the STS API.</div><div><br></div><div>Yes, it does solve my needs (if we ignore the exception handling sharp edges and the ergonomics). It's proved by you already, because what I want - the functional API, can be implemented as a functionality-reducing wrapper of the current STS API.</div><div><br></div><div>I think my type of "yeah, but my use case is so simple, I don't need this powerful tool" feedback, raising a use case that exceeds the capability of the functional API, yet is still considered common, would have been convincing?</div><div><br></div><div>That's what most API designs use, to gate every complexity, every feature with two questions:</div><div><ol><li>What does it really solve that existing, simpler API can't solve well?</li><li>Is this use case compelling enough to pull the weight?</li></ol><div><br></div></div><div>Thanks for the pointer to the wiki, Peter. I'm browsing it now. But if anyone has a pointer to the past discussion that's related to "the simpler functional API isn't sufficient", I'll appreciate it! </div><div> </div></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Sun, Oct 12, 2025 at 4:59 PM Peter Eastham <<a href="mailto:petereastham@gmail.com" target="_blank">petereastham@gmail.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div dir="ltr">I'll toss my two cents in here as another perspective.<br><br>I understand your point is that the API might be more complex than it needs to be, but I'm struggling to see how. It was brought up earlier, but I'll reiterate that the best feedback comes from real world usage <i>because</i> those use cases provide concrete examples of why a specific feature is (or is not!) needed. While conversations like this are useful, I think your best next step is to either create or find and contribute to some OSS Library that wraps STS. I'm unsure if Apache has one yet, but that's a historical location for wrappers around some sharp edges. You could also continue to iterate on your own personal use cases, the library approach just makes it easier to ensure you aren't being too biased towards your own usage.<br><br>My perspective is that while STS does expose a somewhat complex API with some quirks, it's <i>near impossible</i> to achieve all the goals otherwise without complete isolation from the other concurrency models in Java. For example, without some way to populate non-inheritable ThreadLocals STS <b>wouldn't be usable</b> for most applications, as they (and more importantly the libraries they import) weren't designed with STS and ScopedValues in mind. Given that most developers that want to use STS within the next 5 years will be writing with or in existing codebases, that makes sense that any API around it has to be able to accomplish that.<br><br>Your goal of making sure STS isn't more complex than it needs to be <i>is good</i>, I'm hoping my comments above help clarify how you can put your efforts to use for a better ROI.<br>-Peter<br><br>P.S<br>Alan it might be useful for the <a href="https://wiki.openjdk.org/display/loom" target="_blank">Wiki</a> to get some updates around the explored options and where they fell short. I know from my own experience that Wikis are not read as much as they should be, but I can see more comments around the API happening as excitement continues to grow. Just another 2 cents.<br></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Sun, Oct 12, 2025 at 3:56 PM Jige Yu <<a href="mailto:yujige@gmail.com" target="_blank">yujige@gmail.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div dir="ltr"><div dir="ltr"><br></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Sun, Oct 12, 2025 at 12:53 PM Alan Bateman <<a href="mailto:alan.bateman@oracle.com" target="_blank">alan.bateman@oracle.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><u></u>

  
  <div>
    <div>On 12/10/2025 06:32, Jige Yu wrote:<br>
    </div>
    <blockquote type="cite">
      
      <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></div></blockquote><div><br></div><div> Yeah. I've learned that feedbacks from tried, real earnest users would be more useful, which sadly I'm not.</div><div><br></div><div> The exception handling part of it was enough for me to want to try something different and this is the angle I came in. I know my feedback is generally negative but they are honest.</div><div><br></div><div>I did try to use mapConcurrent() and tried it out from the structured concurrency aspect. And I've then realized that it doesn't entirely have the two most important properties: fail-fast and happens-before. It does however provide two-way cancellation and task interruptions.</div><div><br></div><div>I've also gotten my feet wet in trying to implement what I had proposed, making sure at least I know what I'm talking about, fwiw.</div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div>
    <br>
    <blockquote type="cite">
      <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">
      <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></div></blockquote><div><br></div><div>I understand that. And I'm not proposing to add exception type parameters. Those aren't gonna work.</div><div><br></div><div>I was hoping Java could add some help to make exception tunneling easier (I had some detailed clarification in my reply to Remi),</div><div><br></div><div>But even failing that, 3 points are orthogonal to adding type parameters:</div><div><ol><li>Should the callback be Callable or Supplier? With Callable (and with FailedException being unchecked), it's essentially a sneaky exception unchecker. Whereas Supplier would be more like Stream, still not going to make everyone happy, but it's at least honest: won't silently uncheck-ify exceptions.</li><li>Forcing callers to catch or handle InterruptedException is not helpful. mapConcurrent() on the other hand doesn't, which I believe is a better model.</li><li>anySuccessfulResultThrow() swallows runtime exceptions and errors. This to me seems like an anti-pattern.</li></ol></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div>
    <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></div></blockquote><div>I know. But the thought that a standard JDK API would silently swallow <b>by default</b> still feels scary.</div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div>
    <br>
    <br>
    <blockquote type="cite">
      <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></div></blockquote><div><br></div><div>I guess my feedback was at a higher level than the details in the Joiner API. My question was: is the Joiner/STS API even the right API that pulls this weight? If the STS team only needed to make mapConcurrent() fully structured-concurrency, and it only needed to provide a simple, functional API, the API would be a lot simpler and all of these extra imperative concepts like subtasks, joiners, lifecycle callbacks etc. might not even need to exist.</div><div><br></div><div>It's quite likely that the Loom team had already discussed and reached the conclusion that a functional API similar to what I had described, despite being simpler, would not be sufficient, and the extra weight in the current STS is worth it (for reasons X, Y and Z).  If that's the case, then consider my questions dismissed. </div><div><br></div><div>Otherwise, I just want to make sure the unpopular question (<b>is it worth it to build the imperative, complex API?</b>) is on the table.</div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div><br>
    <br>
    <br>
    <blockquote type="cite">
      <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></div></blockquote><div><br></div><div>I guess I got my impression from recent online discussions that people can be keen on using these lifecycle callbacks to bake in business-specific needs. </div><div><br></div><div>It's the thing with these generic libraries though: they can be used, and they can be abused. And imho "how can it avoid being abused" should also be a critical part of designing an API.</div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div>
    <br>
    <br>
    <blockquote type="cite">
      <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><span>
                <div>
                  <div><span>Java</span>
                    <div><button aria-label="Copy code"><span></span><span role="img" aria-hidden="true"></span><span></span><span></span></button></div>
                  </div>
                  <div>
                    <div>
                      <pre><code role="text"><span>// Ideal API for the 80% use case</span>
Robot robot = Concurrently.call(
    () -> fetchArm(),
    () -> fetchLeg(),
    (arm, leg) -> <span>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></div></blockquote><div><br></div><div>Yes. I understand it can be built on top of STS. But my point is to ask: <b>could it be that the simpler API is all that most people need?</b></div><div><br></div><div>There's immense power in the <b>default option</b> provided by the standard JDK. If STS is the default provided by Loom, I'm sure it'll be what majority of people use, even if technically one can build a simpler wrapper - it takes an extra dependency, or it takes extra work, and all the documents are about the default option, so in the end, the theoretical simpler alternative wrapper may not get a chance.</div><div><br></div><div>But there are two potential downsides:</div><div><ol><li>It changes the perception from SC being really easy in Java to something less punchy. The ease-of-use of an API is imho much more important than its raw power.</li><li>The overly powerful STS API, with its sharp edges (e.g. anySuccessfulOrThrow swallows exceptions) can be abused, generating code that's less maintainable in the long run.</li></ol><div>And by asking that question, I guess my daring proposal (out of my average-user naivety) is to decouple the two:</div></div><div><ul><li>Provide a simple, functional API for the 90% users to enjoy SC in the simplest possible way. <b>Forget about power and max coverage in this phase</b>.</li><li>Take the meaty STS API as an "advanced, follow-up project" and evaluate the ROI, given 90% use cases already satisfied by the functional API.</li></ul></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div>
    <br>
    <br>
    <blockquote type="cite">
      <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><span>
                <div>
                  <div><span>Java</span>
                    <div><button aria-label="Copy code"><span></span><span role="img" aria-hidden="true"></span><span></span><span></span></button></div>
                  </div>
                  <div>
                    <div>
                      <pre><code role="text"><span>// Pseudo-code for a recoverable race using a stream gatherer</span>
<T> <span>T <span>race</span><span>(Collection<Callable<T>> tasks, <span>int</span> maxConcurrency)</span> </span>{
    <span>var</span> exceptions = <span>new</span> ConcurrentLinkedQueue<RpcException>();
    <span>return</span> tasks.stream()
        .gather(mapConcurrent(maxConcurrency, task -> {
            <span>try</span> {
                <span>return</span> task.call();
            } <span>catch</span> (RpcException e) {
                <span>if</span> (isRecoverable(e)) { <span>// Selectively recover</span>
                    exceptions.add(e);
                    <span>return</span> <span>null</span>; <span>// Suppress and continue</span>
                }
                <span>throw</span> <span>new</span> RuntimeException(e); <span>// Fail fast on non-recoverable</span>
            }
        }))
        .filter(Objects::nonNull)
        .findFirst() <span>// Short-circuiting and cancellation</span>
        .orElseThrow(() -> <span>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">
      <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>
  </div>

</blockquote></div></div>
</blockquote></div>
</blockquote></div>
</blockquote></div>
</blockquote></div>