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);
     }
}

Reply via email to