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]>
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)