Well said.
> On Oct 12, 2025, at 12:33 AM, Jige Yu <[email protected]> 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. > > High-Level Impression > > From this perspective, the current API feels imperative and more complex for > the common intended use cases than necessary. It introduces significant > cognitive load through its stateful nature and manual lifecycle management. > > Specific Points of Concern > > 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. > > 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. > > 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. > > 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. > > Suggestions for a Simpler Model > > My preference is that the API for the most common use cases should be more > declarative and functional. > > 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) > ); > 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. > > 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. > > I realize my perspective is heavily biased towards the 'everyday' use case > and I may not realize or appreciate the full scope of problems the JEP aims > to solve. And I used a lot of "feels". ;-> > > Anyhow, please forgive ignorance and disregard any points that don't align > with the project's broader vision. > > Thank you again for your dedication to moving Java forward.
