Rethinking Exceptions in the Context of Loom and Structured Concurrency

Eric Kolotyluk eric at kolotyluk.net
Sat Dec 20 03:52:08 UTC 2025


For clarity, this line of thought originally arose from questions around 
outcome aggregation and failure representation in the context of Loom’s 
StructuredTaskScope, particularly when reasoning about multiple 
concurrent subtasks and their combined results. The sketch above is not 
intended to suggest changes to Loom or to structured concurrency APIs, 
but simply reflects how thinking through that specific context led me to 
explore more general value-oriented outcome modeling as a way to reason 
about the trade-offs discussed.

I’m not proposing this as a solution, nor suggesting that such an 
abstraction should exist; this was simply a way for me to think more 
concretely about the design space and the trade-offs discussed in the 
thread. If nothing else, I found the exercise useful for clarifying 
where value-oriented outcomes align well with Java’s existing idioms, 
and where they introduce tension. I’ll defer to the architects’ judgment 
on whether any of this is worth revisiting now or in the future, and 
appreciate the perspectives shared in helping me better understand the 
constraints involved.

Note: I am content with these */not/*being extensible by mere mortals, 
but maintained and extended through the exclusive diligence of the Java 
Language Architects.

Even when using value-oriented outcomes such as Result, callers must 
still be prepared for exceptions to be thrown, whether due to 
programming errors, contract violations, cancellation, or JVM-level 
conditions. This sketch is not intended to eliminate or replace Java’s 
exception model, but to complement it where expected failure can be 
modeled explicitly as data.

    package java.util;

    import java.util.function.Function;

    public sealed interface Result<V, F>
             permits Result.Success, Result.Failure {

         // --- Variants ---

         record Success<V, F>(V value) implements Result<V, F> {}
         record Failure<V, F>(F failure) implements Result<V, F> {}

         // --- Constructors ---

         static <V, F> Result<V, F> success(V value) {
             return new Success<>(value);
         }

         static <V, F> Result<V, F> failure(F failure) {
             return new Failure<>(failure);
         }

         // --- Introspection ---

         default boolean isSuccess() {
             return this instanceof Success<?, ?>;
         }

         default boolean isFailure() {
             return this instanceof Failure<?, ?>;
         }

         // --- Core transforms ---

         default <U> Result<U, F> map(Function<? super V, ? extends U>
    mapper) {
             return switch (this) {
                 case Success<V, F>(var value) ->
                         Result.success(mapper.apply(value));
                 case Failure<V, F>(var failure) ->
                         Result.failure(failure);
             };
         }

         default <G> Result<V, G> mapFailure(Function<? super F, ?
    extends G> mapper) {
             return switch (this) {
                 case Success<V, F>(var value) ->
                         Result.success(value);
                 case Failure<V, F>(var failure) ->
                         Result.failure(mapper.apply(failure));
             };
         }

         default <U> Result<U, F> flatMap(
                 Function<? super V, ? extends Result<U, F>> mapper) {
             return switch (this) {
                 case Success<V, F>(var value) ->
                         mapper.apply(value);
                 case Failure<V, F>(var failure) ->
                         Result.failure(failure);
             };
         }

         // --- Extraction ---

         default V orElse(V fallback) {
             return switch (this) {
                 case Success<V, F>(var value) -> value;
                 case Failure<V, F>(var failure) -> fallback;
             };
         }

         default V orElseGet(Function<? super F, ? extends V> fallback) {
             return switch (this) {
                 case Success<V, F>(var value) -> value;
                 case Failure<V, F>(var failure) -> fallback.apply(failure);
             };
         }

         default <X extends Throwable> V orElseThrow(
                 Function<? super F, ? extends X> toException) throws X {
             return switch (this) {
                 case Success<V, F>(var value) -> value;
                 case Failure<V, F>(var failure) ->
                         throw toException.apply(failure);
             };
         }

         // --- Optional interop ---

         static <T, F> Result<T, F> fromOptional(Optional<T> optional, F
    failureIfEmpty) {
             return optional
                     .<Result<T, F>>map(Result::success)
                     .orElseGet(() -> Result.failure(failureIfEmpty));
         }

         static <T, F> Result<T, F> requirePresent(
                 Result<Optional<T>, F> result,
                 F failureIfEmpty) {

             return result.flatMap(opt ->
                     opt.<Result<T, F>>map(Result::success)
                        .orElseGet(() -> Result.failure(failureIfEmpty)));
         }

         // --- Exception bridge ---

         @FunctionalInterface
         interface ThrowingSupplier<T> {
             T get() throws Throwable;
         }

         static <T> Result<T, Throwable> catching(ThrowingSupplier<T>
    supplier) {
             try {
                 return Result.success(supplier.get());
             } catch (Throwable t) {
                 return Result.failure(t);
             }
         }

         // --- Lossy projections ---

         default Optional<V> success() {
             return switch (this) {
                 case Success<V, F>(var value) ->
    Optional.ofNullable(value);
                 case Failure<V, F>(var failure) -> Optional.empty();
             };
         }

         default Optional<F> failure() {
             return switch (this) {
                 case Success<V, F>(var value) -> Optional.empty();
                 case Failure<V, F>(var failure) ->
    Optional.ofNullable(failure);
             };
         }
    }

example

    switch (result) {
         case Result.Success(var value)   -> use(value);
         case Result.Failure(var failure) -> handle(failure);
    }

Notes:

  * The name java.util.Result may collide with the existing
    java.xml.transform.Result (formerly javax.xml.transform.Result).
    This is not a hard conflict, but it does introduce potential import
    ambiguity and would need to be weighed.
  * The example switch uses record patterns and pattern matching for
    switch, which assumes availability of the relevant language features
    (currently preview / release-dependent).
  * Null handling is unspecified. As written, both Success(null) and
    Failure(null) are possible, and the lossy projections (success() /
    failure()) would treat null as absence. A real API would likely want
    to either forbid nulls or explicitly document this behavior.
  * The instance methods success() and failure() (lossy projections to
    Optional) share names with the static factory methods success(V) /
    failure(E). This is legal but may be a source of confusion and could
    merit different naming.
  * The catching helper currently captures all Throwable, including
    Error. This is intentional for discussion, but in a real API it
    would likely raise questions about whether to catch Exception only,
    or to offer separate variants.
  * The ThrowingSupplier interface is included as a convenience
    placeholder. The JDK may or may not want to introduce an additional
    throwing functional interface in java.util.
  * Providing mapFailure naturally raises questions about whether
    complementary recovery helpers (e.g., recover, recoverWith) should
    exist. They are intentionally omitted here to keep the surface area
    minimal.
  * The Optional interop helpers (fromOptional, requirePresent) are
    intended to clarify the distinction between “operation failed” and
    “operation succeeded but value is absent,” rather than to suggest a
    particular modeling style.

Cheers, Eric

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/loom-dev/attachments/20251219/4f990dfb/attachment.htm>


More information about the loom-dev mailing list