My two cents… Rust’s error handling is horrible - it is designed to work in functional contexts, so like Java streams - the error handling feels “random” (and finding out where the error actually occurred is extremely difficult since it is a value type).
Java’s Exceptions are for ‘exceptional conditions’ and should not be used for flow control (which I don’t think they are in this case - they signify unexpected error conditions). > On Dec 18, 2025, at 3:24 PM, Eric Kolotyluk <[email protected]> wrote: > > My $0.02 > > Why are we still relying so heavily on exceptions as a control-flow mechanism? > > Consider the current StructuredTaskScope design: > > The join() method waits for all subtasks to succeed or any subtask to fail. > The join() method returns null if all subtasks complete successfully. > It throws StructuredTaskScope.FailedException if any subtask fails, with the > exception from the first subtask to fail as the cause. > > This design encodes normal outcomes as null and expected failure modes as > exceptions. That choice forces callers into the least informative and least > composable error-handling model Java has. > > Returning null for success is especially problematic. null conveys no > semantic information, cannot carry context, and pushes correctness checks to > runtime. It remains one of Java’s most damaging design decisions, and Loom > should not be perpetuating it. > > Optional<T> exists, but it is only a partial solution and does not address > error information. In this context, even Optional<Void> would be an > improvement over null, but it still leaves failure modeled exclusively as > exceptional control flow. > > I also want to be clear that I am not confusing try-with-resources with > exceptions. StructuredTaskScope being AutoCloseable is the right design > choice for lifetime management and cancellation, and try blocks are the > correct mechanism for that. However, scope lifetime and outcome reporting are > separable concerns. The use of try does not require that task outcomes be > surfaced exclusively via thrown exceptions. > > As a recent Rust convert, the contrast is stark. Rust’s Result<T, E> treats > failure as a first-class, explicit outcome, enforced by the type system. Java > doesn’t need to abandon exceptions—but it does need to support alternate > paradigms where failure is expected, structured, and composable. > > APIs like join() should envision a future beyond “success = null, failure = > throw”. Even a simple structured outcome type—success or failure—would be a > step forward. Exceptions could remain available for truly exceptional > conditions, not routine concurrency outcomes. > > Loom is a rare opportunity to modernize not just how Java runs concurrent > code, but how Java models correctness and failure. Re-entrenching null and > exception-only outcomes misses that opportunity. > > I’ll stop bloviating now. > > Sincerely, > Eric Kolotyluk > > > > On 2025-12-18 1:00 PM, David Alayachew wrote: > >> For 1, the javadoc absolutely does help you. Please read for open. >> >> https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/StructuredTaskScope.html#open() >> >> As for verbose, can you go into more detail? This is a traditional builder >> pattern addition, so it is literally 1 static method call. >> >> That said, if you dislike a 0 parameter call being forced into being a 2 >> paramefer call when you need to add timeout, then sure, I think adding an >> overload for that static method that takes in the configFunction is >> reasonable. I'd support that. >> >> >> On Thu, Dec 18, 2025, 3:46 PM Holo The Sage Wolf <[email protected] >> <mailto:[email protected]>> wrote: >>> Hello Loom devs, >>> Few years ago I experimented in a personal PoC project with >>> StructuredConcurrency in Java 19 and I had to stop working on it for >>> personal reasons. >>> >>> Recently I came back to the project and updated it to Java 25 and had to >>> change my code to the new way the API is built and while doing that I >>> noticed a couple of stuff I want to point out: >>> >>> 1. The default Joiner method can't receive timeout >>> Obviously that is wrong, but the API and JavaDoc don't actually help you. >>> Say you start with: >>> ```java >>> try (var scope = StructuredTaskScope.open()) { >>> ... >>> } >>> ``` >>> And I want to evolve the code to add timeout, I look at the >>> StructuredTaskScope static methods, and won't see any way to do that. After >>> reading a bit what StructuredTaskScope.open(Joiner, configFunction) does, I >>> will realise that I can set the timeout using the configFunction. >>> But then I will encounter the problem that I need to provide a Joiner, >>> currently the only way to actually get the "no args method"-joiner is to >>> look at the source code of the method, see which Joiner it uses and copy >>> that into my method to get: >>> ```java >>> try (var scope = >>> StructuredTaskScope.open(Joiner.awaitAllSuccessfulOrThrow(), (conf) -> >>> ...)) { >>> ... >>> } >>> ``` >>> Not only is this a lot of work to do something very simple, there is a high >>> chance that people who start learning concurrency will want to use timeout >>> before they even know what the Joiner object is. >>> >>> 2. Changing only the timeout is "verbose". >>> I can only talk from my experience, so I may have the wrong impression, but >>> I feel like setting timeout is orders of magnitude more common than >>> changing the default ThreadFactory (especially when using virtual threads) >>> or setting a name. >>> I feel like adding a couple of overloads of the open method that takes only >>> an extra parameter of duration will be convenient: >>> > StructuredTaskScope.open() >>> > StructuredTaskScope.open(Duration timeout) >>> > StructuredTaskScope.open(Joiner joiner) >>> > StructuredTaskScope.open(Joiner joiner, Duration timeout) >>> > StructuredTaskScope.open(Joiner joiner, Function<Configuration, >>> > Configuration> configFunction) >>>
