Rethinking Exceptions in the Context of Loom and Structured Concurrency
Holo The Sage Wolf
holo3146 at gmail.com
Fri Dec 19 09:39:08 UTC 2025
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.Lookup lookup = MethodHandles.lookup();
private static final VarHandle FIRST_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 Throwable firstException;
@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);
}
}
On Fri, Dec 19, 2025 at 11:10 AM Davor Hrg <hrgdavor at gmail.com> wrote:
> I have also been experimenting with exceptions as values.
>
> It served me well for some initial experiments with wrapping HTTP
> fetch-like utilities.
>
> One issue came up: It was fine while I was not interested in what
> exception happens,
> but as soon as I wanted to check multiple exception types I had to resort
> to Throwable.
>
> My exploration is still superficial, but I have not found a way to define
> exception as value
> where it can define what "throws IoException,ValidationError" can. we
> would maybe need union types. ...
> and I am also afraid what happy when we start mixing the two concepts all
> over the codebases.
>
> This exploration of errors as types also made me appreciate Exceptions bit
> more.
>
>
>
>
>
>
>
> On Fri, Dec 19, 2025 at 7:35 AM Eric Kolotyluk <eric at kolotyluk.net> wrote:
>
>> Hi all,
>>
>> I’m starting a new thread to continue a discussion that emerged
>> elsewhere, per mailing list etiquette, and to give the topic a clean and
>> traceable home.
>>
>> My interest here isn’t reactive to any one exchange. I’ve been
>> experimenting with Loom since its early iterations, and over time it has
>> sharpened a concern I already had: whether Java’s traditional exception
>> model remains the right default abstraction in a world of structured
>> concurrency, virtual threads, and large-scale composition.
>>
>> To be clear, this is not a claim that “exceptions are broken” or that
>> Java should abandon them. Java’s exception system has supported billions of
>> lines of successful code, and I’ve used it productively for decades.
>> Rather, Loom makes certain trade-offs more visible — particularly around
>> control flow, cancellation, failure propagation, and reasoning about
>> lifetimes — that were easier to ignore in a purely thread-per-task world.
>>
>> The core questions I’m interested in exploring are along these lines:
>>
>> - How do unchecked exceptions interact with structured concurrency’s
>> goal of making lifetimes and failure scopes explicit?
>> - Do exceptions remain the best abstraction for expected failure in
>> highly concurrent, compositional code?
>> - Are there patterns (or emerging idioms) that Loom encourages which
>> mitigate long-standing concerns with exceptions — or does Loom expose new
>> ones?
>> - More broadly, should Java be thinking in terms of additional
>> failure-handling tools rather than a single dominant model?
>>
>> I’m not advocating a specific alternative here — just inviting a
>> technical discussion about whether Loom changes how we should think about
>> error handling, and if so, how.
>>
>> That said, exposure to other ecosystems (e.g., Scala, Kotlin, and more
>> recently Rust) has broadened how I think about failure modeling. One thing
>> I’ve consistently appreciated about Java is that it tends to integrate
>> external ideas deliberately, rather than reflexively rejecting them or
>> adopting them wholesale. Loom itself is a good example of that approach.
>>
>> I’m interested in whether error handling deserves a similar
>> re-examination in light of Loom’s goals.
>>
>> Looking forward to the discussion.
>>
>> Cheers,
>> Eric
>>
>
--
Holo The Wise Wolf Of Yoitsu
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/loom-dev/attachments/20251219/7756137a/attachment-0001.htm>
More information about the loom-dev
mailing list