<!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>