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 <[email protected]> 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 <[email protected]> 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 <[email protected]> >> 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 <[email protected]> wrote: >>> >>>> >>>> >>>> On Sun, Oct 12, 2025 at 12:53 PM Alan Bateman <[email protected]> >>>> 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 >>>>> >>>>> >>>>>
