Feedback on Structured Concurrency (JEP 525, 6th Preview)
Jige Yu
yujige at gmail.com
Mon Oct 13 01:56:54 UTC 2025
Understood your perspective, Peter. Thanks for the response!
I think my earlier comment could be interpreted the wrong way.
So if I may take it back to phrase it more accurately, this feedback is
more like:
A swiss army knife is what I need everyday. This jigsaw *can* cut for me,
> but the sharp edges and the unwieldiness are my concern.
On Sun, Oct 12, 2025 at 6:47 PM Peter Eastham <petereastham at gmail.com>
wrote:
> > 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?
>
> 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 *how bad* those aspects are. Since we have a mature preview API,
> it enables people to provide more than thoughts.
>
> 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".
> -Peter
>
> On Sun, Oct 12, 2025 at 7:06 PM Jige Yu <yujige at gmail.com> wrote:
>
>> Yeah, I understand by not really having used the STS api seriously, I
>> must have limited my understanding of it in some ways.
>>
>> 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.
>>
>> 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.
>>
>> But here I'm not having a new use case or asking for a new feature.
>>
>> I'm simply saying that for all the use cases I can visualize, I only need
>> a very limited subset of the STS API.
>>
>> 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.
>>
>> 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?
>>
>> That's what most API designs use, to gate every complexity, every feature
>> with two questions:
>>
>> 1. What does it really solve that existing, simpler API can't solve
>> well?
>> 2. Is this use case compelling enough to pull the weight?
>>
>>
>> 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!
>>
>>
>> On Sun, Oct 12, 2025 at 4:59 PM Peter Eastham <petereastham at gmail.com>
>> wrote:
>>
>>> I'll toss my two cents in here as another perspective.
>>>
>>> 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
>>> *because* 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.
>>>
>>> My perspective is that while STS does expose a somewhat complex API with
>>> some quirks, it's *near impossible* 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 *wouldn't
>>> be usable* 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.
>>>
>>> Your goal of making sure STS isn't more complex than it needs to be *is
>>> good*, I'm hoping my comments above help clarify how you can put your
>>> efforts to use for a better ROI.
>>> -Peter
>>>
>>> P.S
>>> Alan it might be useful for the Wiki
>>> <https://wiki.openjdk.org/display/loom> 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.
>>>
>>> On Sun, Oct 12, 2025 at 3:56 PM Jige Yu <yujige at gmail.com> wrote:
>>>
>>>>
>>>>
>>>> On Sun, Oct 12, 2025 at 12:53 PM Alan Bateman <alan.bateman at oracle.com>
>>>> wrote:
>>>>
>>>>> On 12/10/2025 06:32, Jige Yu 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.
>>>>>
>>>>> 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.
>>>>>
>>>>
>>>> Yeah. I've learned that feedbacks from tried, real earnest users would
>>>> be more useful, which sadly I'm not.
>>>>
>>>> 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.
>>>>
>>>> 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.
>>>>
>>>> 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.
>>>>
>>>>>
>>>>> ------------------------------
>>>>>
>>>>> 1.
>>>>>
>>>>> *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.
>>>>>
>>>>> 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.
>>>>>
>>>>>
>>>>>
>>>>> 1.
>>>>>
>>>>> *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.
>>>>>
>>>>> 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.
>>>>>
>>>>
>>>> I understand that. And I'm not proposing to add exception type
>>>> parameters. Those aren't gonna work.
>>>>
>>>> I was hoping Java could add some help to make exception tunneling
>>>> easier (I had some detailed clarification in my reply to Remi),
>>>>
>>>> But even failing that, 3 points are orthogonal to adding type
>>>> parameters:
>>>>
>>>> 1. 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.
>>>> 2. Forcing callers to catch or handle InterruptedException is not
>>>> helpful. mapConcurrent() on the other hand doesn't, which I believe is a
>>>> better model.
>>>> 3. anySuccessfulResultThrow() swallows runtime exceptions and
>>>> errors. This to me seems like an anti-pattern.
>>>>
>>>>
>>>>> 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.
>>>>>
>>>>> I know. But the thought that a standard JDK API would silently swallow *by
>>>> default* still feels scary.
>>>>
>>>>>
>>>>>
>>>>>
>>>>> 1.
>>>>>
>>>>> *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.
>>>>>
>>>>> join always returns something. For allSuccessfulOrThrow it returns a
>>>>> stream of successful subtasks.
>>>>>
>>>>> 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.
>>>>>
>>>>
>>>> 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.
>>>>
>>>> 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.
>>>>
>>>> Otherwise, I just want to make sure the unpopular question (*is it
>>>> worth it to build the imperative, complex API?*) is on the table.
>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>> 1.
>>>>>
>>>>> *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.
>>>>>
>>>>> 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.
>>>>>
>>>>
>>>> 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.
>>>>
>>>> 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.
>>>>
>>>>>
>>>>>
>>>>> ------------------------------
>>>>> *Suggestions for a Simpler Model*
>>>>>
>>>>> My preference is that the API for the most common use cases should be
>>>>> more *declarative and functional*.
>>>>>
>>>>> 1.
>>>>>
>>>>> *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)
>>>>> );
>>>>>
>>>>>
>>>>>
>>>>> 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.
>>>>>
>>>>> <U, V, R> R callConcurrently(Callable<U> task1, Callable<V> task2,
>>>>> BiFunction<U, V, R> combine) {
>>>>> try (var scope = StructuredTaskScope.open()) {
>>>>> Supplier<U> subtask1 = scope.fork(task1);
>>>>> Supplier<V> subtask2 = scope.fork(task2);
>>>>> scope.join();
>>>>> return combine.apply(subtask1.get(), subtask2.get());
>>>>> }
>>>>> }
>>>>>
>>>>> (there's a more general form of the example presented in the JEP),
>>>>>
>>>>
>>>> Yes. I understand it can be built on top of STS. But my point is to
>>>> ask: *could it be that the simpler API is all that most people need?*
>>>>
>>>> There's immense power in the *default option* 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.
>>>>
>>>> But there are two potential downsides:
>>>>
>>>> 1. 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.
>>>> 2. 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.
>>>>
>>>> And by asking that question, I guess my daring proposal (out of my
>>>> average-user naivety) is to decouple the two:
>>>>
>>>> - Provide a simple, functional API for the 90% users to enjoy SC in
>>>> the simplest possible way. *Forget about power and max coverage in
>>>> this phase*.
>>>> - 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.
>>>>
>>>>
>>>>>
>>>>>
>>>>> 1.
>>>>>
>>>>> *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.
>>>>>
>>>>>
>>>>> 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.
>>>>>
>>>>> We do want to bring mapConcurrent (or a successor) into the structured
>>>>> fold but don't have a good proposal at this time.
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>> 1.
>>>>>
>>>>> *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.
>>>>>
>>>>> 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.
>>>>>
>>>>> -Alan
>>>>>
>>>>>
>>>>>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/loom-dev/attachments/20251012/e9e8d7fa/attachment-0001.htm>
More information about the loom-dev
mailing list