Upon reviewing the current documentation, I see that I misstated an
earlier point. StructuredTaskScope.join() returns a result of type R (as
produced by the configured Joiner), rather than returning null
unconditionally on success.
That said, the outcome contract remains: join() either returns a value
or throws. After waiting for completion or cancellation, the scope
invokes Joiner.result(); if that method throws, join() throws
FailedException with the underlying exception as the cause (with
timeouts and cancellation also surfaced as exceptions).
So while Joiners make join() result-producing and more configurable, the
failure channel is still exception-based. From that perspective, I can
still see value in a functional, value-oriented result type—where
success and failure are both represented explicitly as values—coexisting
alongside exceptions, rather than routing expected failure exclusively
through throws.
Joiners improve policy flexibility, but they don’t quite address that
particular concern.
Respectfully,
Eric Kolotyluk
On 2025-12-18 4:46 PM, Eric Kolotyluk wrote:
Respectfully, I think we’re talking past each other a bit.
Calling Rust’s error handling “horrible” is a subjective judgment
about trade-offs, not an objective flaw. Rust’s Result<T, E> is
deliberately value-oriented and explicit; Java’s exception model is
deliberately stack-oriented and implicit. Each optimizes for different
things, and each has real costs.
My appreciation of Java’s evolution is that it has consistently
expanded the set of available tools, rather than insisting on a single
paradigm. Generics, lambdas, streams, records, sealed types, Optional,
and now Loom itself all reflect that trajectory. They didn’t replace
older mechanisms; they complemented them.
There has been sustained criticism in the Java community of both null
and over-reliance on exceptions, particularly where failure is
expected rather than exceptional. I’m not here to relitigate either
debate, nor to argue that exceptions should go away. My point is
simply that other options exist, and Java has historically been at its
best when APIs acknowledge and support them.
In that light, my concern with StructuredTaskScope.join() is not that
it uses exceptions at all, but that it offers only an exception-based
outcome model, with null representing success. That feels like a
missed opportunity in an otherwise forward-looking API.
I’m advocating for additional, not replacement, abstractions—ones that
allow structured concurrency outcomes to be expressed explicitly when
appropriate, while leaving exceptions fully available for genuinely
exceptional conditions.
Respectfully,
Eric Kolotyluk
On 2025-12-18 1:34 PM, Robert Engels wrote:
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]> 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)