Checked exceptions are great. Just need to understand how to use them properly. I think the Swift way of addressing it is fantastic.
> On Oct 12, 2025, at 8:28 AM, Remi Forax <[email protected]> wrote: > > > > > From: "Jige Yu" <[email protected]> > To: "loom-dev" <[email protected]> > Sent: Sunday, October 12, 2025 7:32:33 AM > Subject: Feedback on Structured Concurrency (JEP 525, 6th Preview) > Hi Project Loom. > > > Hello, > > 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. > > > I had a similar feeling the first time I used the API, but once you play with > it, it kind of make sense. > The API can be used when all tasks are different (concurrent tasks) or when > all task are the same (parallel tasks), a more functional API will only work > with the latter. > > > 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. > > > You can propagate the exceptions but it makes the API clunkier (one more type > variable everywhere) and do not solve the fundamental problem that you do not > want to merge the control flow of an exception that comes from a callable > with one that comes from STS.join(). By example, distinguishing if an > InterruptedException is raised because the main thread is interrupted or if > one of the callable is interrupted (and this is the same will all runtime > exceptions). > > 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. > > > see above > > 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. > > > Having InterruptedException not being runtime exception is a pain. But this > is a pain for all blocking methods. > And BTW, you can also wrap it into a runtime exception (usually > UncheckedIOException/IOError) which works better than > currentThread().interrupt() because you do not loose the context (the stack > trace) and avoid the problem of the signature propagation. > > Perhaps at some point in the future, all exceptions will be runtime > exceptions (like in Kotlin or C#) but this is a Java problem not a problem of > the STS API. > > > 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. > > > The straightforward mechanism is to inspect the Subtasks that keep that > information (if available). > > 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. > > > I kind agree on this one, i.e. i would like the semantics of when to stop the > STS and the semantics of getting all subtaks or not to be separated given > there are separated concern. > > 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. > > > yes, especially if you try to do reduce to a value (like a Collector) inside > the Joiner but this is called out by the documentation. > > I will answer the rest of the mail, in a new message. > > Rémi > > >
