Timeouts in structured concurrency

David Alayachew davidalayachew at gmail.com
Fri Dec 19 02:02:56 UTC 2025


@Eric Kolotyluk <eric at kolotyluk.net> - mailing list etiquette dictates that
a new topic should be a new thread. @Robert Engels <robaho at me.com> probably
sent an email to you privately to avoid breaking etiquette.

Since this conversation already started, just make a new thread, and then
link back to this conversation via the archive post, then just continue the
discussion where you left off.

And to be clear, this discussion is absolutely worth having. You are making
some decent points, and I have some to share. But for traceability sake,
let's not hijack @Holo The Sage Wolf <holo3146 at gmail.com>'s thread. Email
me separately if you need help linking this conversation via archive post
in the new thread.

On Thu, Dec 18, 2025 at 8:50 PM Robert Engels <robaho at me.com> wrote:

> 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.
>
> On Dec 18, 2025, at 6:46 PM, Eric Kolotyluk <eric at kolotyluk.net> 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 <eric at kolotyluk.net>
> <eric at kolotyluk.net> 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 <holo3146 at gmail.com>
> 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)
>>
>>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/loom-dev/attachments/20251218/ad4af6a8/attachment-0001.htm>


More information about the loom-dev mailing list