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