Feedback on Structured Concurrency (JEP 525, 6th Preview)

Jige Yu yujige at gmail.com
Mon Oct 13 04:22:21 UTC 2025


At the risk of a tangent, recently there was a discussion thread about a
use case of web crawler and having different forks inter-communicating with
the parent scope being blocked etc.

Imho, this may be a case of the imperative STS API encouraging users toward
complex solutions.

If there were nothing but a functional approach, I'd think the use case can
be trivially implemented with a mapConcurrent() that fully supports
structured concurrency :

int maxConcurrency = 10;
Set<String> seen = new HashSet<>();
seen.add(root);
for (List<String> toCrawl = List.of(root); toCrawl.size() > 0; ) {
  toCrawl = toCrawl.stream()
      .gather(mapConcurrent(maxConcurrency, url -> loadWebPage(url)))
      .flatMap(page -> page.getLinks().stream())
      .filter(seen::add)
      .toList();
}

The code essentially does a breadth-first traversal, at each round
concurrently crawls web pages with a map concurrency limit.

And this is the direction I'd hope the Loom team can more seriously
entertain: put a strong constraint on the API's imperative power, run a
thought experiment to see if the functional variant could offer sufficient
flexibility under the constraint.

And along the way, make exceptions easier to use.

On Sun, Oct 12, 2025 at 6:56 PM Jige Yu <yujige at gmail.com> wrote:

> 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/ef6dd476/attachment-0001.htm>


More information about the loom-dev mailing list