<html class="apple-mail-supports-explicit-dark-mode"><head><meta http-equiv="content-type" content="text/html; charset=utf-8"></head><body dir="auto"><div dir="ltr"></div><div dir="ltr">Respectfully, no you’re arguing that because structured concurrency is a new addition this is an opportunity to explore using values as errors. That would be a poor choice. Java developers have successfully written billions of lines of code using exceptions. It’s not hard or error prone if you take the time to understand it - and it’s far better than errors as values like Rust/Go/C has. </div><div dir="ltr"><br><blockquote type="cite">On Dec 18, 2025, at 6:46 PM, Eric Kolotyluk <eric@kolotyluk.net> wrote:<br><br></blockquote></div><blockquote type="cite"><div dir="ltr">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
Respectfully, I think we’re talking past each other a bit.<br>
<br>
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.<br>
<br>
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.<br>
<br>
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.<br>
<br>
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.<br>
<br>
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.<br>
<br>
Respectfully,<br>
Eric Kolotyluk
<p><br>
</p>
<div class="moz-cite-prefix">On 2025-12-18 1:34 PM, Robert Engels
wrote:<br>
</div>
<blockquote type="cite" cite="mid:8C2482BF-515C-4A89-8E68-197399E89452@me.com">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
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).
<div><br>
</div>
<div>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).</div>
<div><br>
</div>
<div>
<div><br>
<blockquote type="cite">
<div>On Dec 18, 2025, at 3:24 PM, Eric Kolotyluk
<a class="moz-txt-link-rfc2396E" href="mailto:eric@kolotyluk.net"><eric@kolotyluk.net></a> wrote:</div>
<br class="Apple-interchange-newline">
<div>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<div> My $0.02<br>
<br>
Why are we still relying so heavily on exceptions as a
control-flow mechanism?<br>
<br>
Consider the current StructuredTaskScope design:<br>
<br>
The join() method waits for all subtasks to succeed or
any subtask to fail.<br>
The join() method returns null if all subtasks complete
successfully.<br>
It throws StructuredTaskScope.FailedException if any
subtask fails, with the exception from the first subtask
to fail as the cause.<br>
<br>
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.<br>
<br>
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.<br>
<br>
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.<br>
<br>
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.<br>
<br>
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.<br>
<br>
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.<br>
<br>
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.<br>
<br>
I’ll stop bloviating now.<br>
<br>
Sincerely,<br>
Eric Kolotyluk<br>
<br>
<p><br>
</p>
<p>On 2025-12-18 1:00 PM, David Alayachew wrote:</p>
<blockquote type="cite" cite="mid:CAA9v-_MxzoznSe6EFJ8P44RTQVm6SmVTiuVuQ-nN4xPPZppodw@mail.gmail.com">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<div dir="auto">
<div>For 1, the javadoc absolutely does help you.
Please read for open.
<div dir="auto"><br>
</div>
<div dir="auto"><a href="https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/StructuredTaskScope.html#open()" rel="noreferrer noreferrer" target="_blank" moz-do-not-send="true" class="moz-txt-link-freetext">https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/StructuredTaskScope.html#open()</a></div>
<div dir="auto"><br>
</div>
<div dir="auto">As for verbose, can you go into
more detail? This is a traditional builder
pattern addition, so it is literally 1 static
method call.</div>
<div dir="auto"><br>
</div>
<div dir="auto">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.</div>
<br>
<br>
<div class="gmail_quote">
<div dir="ltr" class="gmail_attr">On Thu, Dec
18, 2025, 3:46 PM Holo The Sage Wolf <<a href="mailto:holo3146@gmail.com" rel="noreferrer noreferrer" target="_blank" moz-do-not-send="true" class="moz-txt-link-freetext">holo3146@gmail.com</a>>
wrote:<br>
</div>
<blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex">
<div dir="ltr">
<div>Hello Loom devs,</div>
<div>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.</div>
<div><br>
</div>
<div>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:</div>
<div><br>
</div>
<div>1. The default Joiner method can't
receive timeout</div>
<div>Obviously that is wrong, but the API
and JavaDoc don't actually help you. Say
you start with:</div>
<div> ```java</div>
<div>try (var scope =
StructuredTaskScope.open()) {</div>
<div> ...</div>
<div>}</div>
<div>```</div>
<div>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.</div>
<div>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:</div>
<div>
<div> ```java</div>
<div>try (var scope =
StructuredTaskScope.open(Joiner.awaitAllSuccessfulOrThrow(), (conf)
-> ...)) {</div>
<div> ...</div>
<div>}</div>
<div>```</div>
<div>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.</div>
<div><br>
</div>
<div>2. Changing only the timeout is
"verbose".</div>
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.</div>
<div>I feel like adding a couple of
overloads of the open method that takes
only an extra parameter of duration will
be convenient:</div>
<div>> StructuredTaskScope.open()</div>
<div>> StructuredTaskScope.open(Duration
timeout)
<div>> StructuredTaskScope.open(Joiner
joiner)</div>
<div>> StructuredTaskScope.open(Joiner
joiner, Duration timeout) </div>
<div>> StructuredTaskScope.open(Joiner
joiner, Function<Configuration,
Configuration> configFunction) </div>
<br>
</div>
</div>
</blockquote>
</div>
</div>
</div>
</blockquote>
</div>
</div>
</blockquote>
</div>
<br>
</div>
</blockquote>
</div></blockquote></body></html>