Rethinking Exceptions in the Context of Loom and Structured Concurrency
Eric Kolotyluk
eric at kolotyluk.net
Fri Dec 19 18:28:49 UTC 2025
Hello Holo,
Thank you for putting this together — this is exactly the kind of
concrete exploration I was hoping might emerge from the discussion. A
working PoC that actually exercises StructuredTaskScope and Joiner
semantics is far more valuable than abstract debate, and I appreciate
the care you took in documenting the caveats.
Taken on its own terms, this convincingly demonstrates an important
point: “exceptions as values” can be expressed today, within the current
JVM and Loom architecture, without special runtime support. Your
implementation makes that clear, and it’s helpful to see how little
machinery is actually required to model “all succeed or capture first
failure” explicitly.
What I find most interesting here isn’t whether this exact Result type
should exist, or whether this particular Joiner is the “right”
abstraction — it’s what the PoC reveals about the design space.
A few reflections, more observational than critical:
* Your implementation makes failure aggregation explicit rather than
ambient. The fact that this logic lives in the Joiner — instead of
being implicit in exception propagation — is precisely what makes
the behavior legible. That’s a real conceptual difference,
regardless of which model one prefers.
* At the same time, the caveats you list are telling. The need to
still catch InterruptedException, the inability to prevent timeout
configuration through ConfigFunc, and the fact that timeout
semantics live outside the Joiner unless reimplemented — all point
to the same underlying reality: failure, cancellation, and time are
still split across multiple semantic channels.
* None of this is a knock on Loom or STS. If anything, it reinforces
how much structured concurrency already clarifies lifetimes and
ownership — and how the remaining friction shows up primarily around
how failure and cancellation are represented and composed.
I also want to emphasize: I’m not reading this as “here is the
alternative Java should adopt.” Rather, I see it as evidence that there
are still open questions worth examining:
* Which aspects of failure benefit most from being explicit and
value-oriented?
* Which aspects genuinely benefit from stack-based propagation and
VM-native support?
* And where does cancellation sit — error, control signal, or
something orthogonal — especially under parallel composition?
Your PoC doesn’t answer those questions definitively, but it sharpens
them considerably. That’s exactly the kind of contribution that makes a
technical discussion productive rather than ideological.
Thanks again for taking the time to build and share this — it’s a strong
piece of evidence that the space is still worth exploring.
Cheers,
Eric
On 2025-12-19 1:39 AM, Holo The Sage Wolf wrote:
> Hello Eric,
>
> Here is a simple implementation of "exception as values" structured
> concurrency *using the current architecture* as a Proof of Concept,
> this is a working example in Java 25 with the flag --enable-preview.
>
> Couple of notes:
> 1. I used the most on the nose naive implementation of Result<Value,
> Error> type just as a Proof of Concept, but this can be replaced with
> a more sophisticated type.
> 2. I didn't implement the timeout feature as "exception as value",
> meaning using this PoC the only way to use timeout is via the
> ConfigFunc which *will* throw an exception on timeout.
> - It is possible and relatively simply to implement timeout inside
> the Joiner and use that instead of the ConfigFunc feature of timeout
> and thus bypass this point
> 3. This is specifically a "on all success return stream of results"
> implementation, this implementation can easily adapted to all other
> usecases via minimal changes
>
> Minus the above caveats, this implementation is production ready.
> The only actual downsides I can think of about this implementation are:
> 1. Even though InterruptedException should never be thrown, you still
> have to catch it
> 2. There is no way to prevent users from setting timeout via ConfFunc
> even if `AllSuccessfulResult` were to implement timeout mechanism
>
> @SuppressWarnings("preview")
> void main() {
> try (var scope = StructuredTaskScope.open(new AllSuccessfulResult<String>())) {
> scope.fork(() ->"Holo");
> scope.fork(() ->"Test");
> var x = scope.join();// x=(stream of ("Holo", "Test"), null), the order of the stream is
> not guarantee
> // scope.fork(() -> "Holo"); // scope.fork(() -> "Test"); //
> scope.fork(() -> { // throw new IllegalStateException(); // }); // var
> x = scope.join(); // x=(null, illegalStateException) }catch (InterruptedException | StructuredTaskScope.TimeoutException e) {
> throw new RuntimeException(e);// only teachable on timeout *when the timeout is configured through
> StructuredTaskScope.open* // To make it so timeout doesn't get into
> here, add into the `AllSuccessfulResult` type built in mechanism of
> timeout // implementing timeout inside the Joiner is not hard, but I
> omitted it from the Proof of Concept }
> }
>
> record Result<V,R extends Throwable>(V value,R exception) {}
>
> @SuppressWarnings("preview")
> static final class AllSuccessfulResult<T> implements StructuredTaskScope.Joiner<T, Result<Stream<T>, Throwable>> {
> private static final MethodHandles.Lookuplookup = MethodHandles.lookup();
> private static final VarHandleFIRST_EXCEPTION;
>
> static {
> try {
> FIRST_EXCEPTION =lookup.findVarHandle(lookup.lookupClass(),"firstException", Throwable.class);
> }catch (NoSuchFieldException | IllegalAccessException e) {
> throw new RuntimeException(e);// shouldn't be possible to reach }
> }
>
> // list of forked subtasks, only accessed by owner thread private final List<StructuredTaskScope.Subtask<T>> subtasks =new ArrayList<>();
>
> private volatile ThrowablefirstException;
>
> @Override public boolean onFork(StructuredTaskScope.Subtask<?extends T> subtask) {
> if (subtask.state() != StructuredTaskScope.Subtask.State.UNAVAILABLE) {// after `join` you can't use onFork throw new IllegalArgumentException("Subtask not in UNAVAILABLE state");
> }
> @SuppressWarnings("unchecked")
> var s = (StructuredTaskScope.Subtask<T>) subtask;
> subtasks.add(s);
> return false;
> }
>
> @Override public boolean onComplete(StructuredTaskScope.Subtask<?extends T> subtask) {
> StructuredTaskScope.Subtask.State state = subtask.state();
> if (state == StructuredTaskScope.Subtask.State.UNAVAILABLE) {
> throw new IllegalArgumentException("Subtask has not completed");// this shouldn't be reachable I think, but just in case }
>
> return (state == StructuredTaskScope.Subtask.State.FAILED)
> && (firstException ==null)
> &&FIRST_EXCEPTION.compareAndSet(this,null, subtask.exception());
> }
>
> @Override public Result<Stream<T>, Throwable> result() {
> if (firstException !=null) {
> return new Result<>(null,firstException);
> }
> var results =subtasks.stream()
> .filter(t -> t.state() == StructuredTaskScope.Subtask.State.SUCCESS)
> .map(StructuredTaskScope.Subtask::get);
> return new Result<>(results,null);
> }
> }
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/loom-dev/attachments/20251219/e97cdba2/attachment.htm>
More information about the loom-dev
mailing list