We are analyzing the behavior of the Java SDK TryExecutor.handleException() implementation (https://github.com/serverlessworkflow/sdk-java/blob/main/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java#L162C3-L197C4). The DSL spec states that an error must propagate when no catch filter/predicate matches the raised error, but the SDK currently returns CompletableFuture.completedFuture(taskContext.rawOutput()) before the filter is evaluated.
Please clarify the intended design with these questions:
-
Unmatched filter flow
- In the code below,
completable is initialized to a completed (successful) future before any error filter or when/exceptWhen check. If the filter or predicate later evaluates to false, the method simply returns that success future instead of propagating the error. Is that intentional (design choice) or a bug? The DSL says the error should propagate (workflow should FAULT) when there is no match.
CompletableFuture<WorkflowModel> completable =
CompletableFuture.completedFuture(taskContext.rawOutput());
if (filterMatches && WorkflowUtils.whenExceptTest(...)) {
// catch.do and/or retry
}
return completable; // always SUCCESS, even when filter does not match
**Example:** TLS workflow raises a timeout error while the catch block is configured for validation errors. The relevant YAML is:
```yaml
do:
- attemptTask:
try:
- failingTask:
raise:
error:
type: https://example.com/errors/timeout
status: 408
title: Request Timeout
catch:
errors:
with:
type: https://example.com/errors/validation
do:
- handleValidation:
set:
recovered: true
```
Despite the timeout, the SDK still returns `CompletableFuture.completedFuture(...)` before seeing the mismatch, so the try task completes instead of faulting.
-
when and exceptWhen predicates
-
Since the predicates short-circuit the condition, a false when or true exceptWhen also causes the if block to be skipped. Yet the method still returns the success future created above. Shouldn’t the error be re-thrown when when/exceptWhen reject the error, per the spec?
Example: The error block raises a 503 while the when predicate only accepts status 400:
do:
- attemptTask:
try:
- failingTask:
raise:
error:
type: https://example.com/errors/transient
status: 503
catch:
as: caughtError
when: ${ .status == 400 }
do:
- handleError:
set:
recovered: true
Because the SDK returns the pre-resolved success future, the error never propagates even though when evaluated to false.
-
catch.do failure propagation
-
The same logic is reused when catch.do contains nested try/catch. If an inner error is not matched, the inner handleException() call returns the already-completed success future, so the catch.do chain never faulted even though the inner error should escape and eventually fault the workflow. Was this swallowing behavior intended?
Example: An outer catch.do contains an inner try that raises an error but its filter rejects it:
do:
- outerTry:
try:
- outerFailingTask:
raise: ...outer error...
catch:
as: outerError
do:
- innerTryInCatchDo:
try:
- innerFailingTask:
raise: ...inner error...
catch:
errors:
with:
type: https://example.com/errors/wrong-type
do:
- innerRecovery: { ... }
The inner error type is https://example.com/errors/inner, so the inner filter does not match. The SDK still returns the success future from the nested handleException(), meaning the outer try thinks catch.do succeeded rather than faulting.
-
Error detail comparison
-
Can you confirm whether this line is a typo? https://github.com/serverlessworkflow/sdk-java/blob/main/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java#L210
// current SDK code
compareString(errorFilter.getDetails(), errorFilter.getDetails())
// expected comparison
compareString(errorFilter.getDetails(), error.details())
-
Why this matters: in the current code the detail filter is compared to itself, so that check is always true when details is present. This makes catch.errors.with.details effectively non-functional, because mismatched error details are never rejected.
-
Example scenario:
do:
- attemptTask:
try:
- failingTask:
raise:
error:
type: https://example.com/errors/security
status: 403
details: User not found in tenant catalog
catch:
errors:
with:
type: https://example.com/errors/security
details: Enforcement Failure - invalid email
do:
- handleSecurity:
set:
recovered: true
-
Expected behavior (DSL): detail mismatch means catch filter does not match, so the error should propagate and the workflow should fault.
-
Current SDK behavior: catch is treated as matched and catch.do runs, so the workflow completes successfully.
-
catch.as binding
-
The caught error object is never injected into the expression context for catch.do (no taskContext or model mutation with the as variable). Is that deliberate, or should the error be bound so ${ .caughtError.detail } works for expressions?
Example: A catch block binds the caught error to caughtError and reads its detail:
do:
- attemptTask:
try:
- failingTask:
raise: ...
catch:
as: caughtError
do:
- handleAnyError:
set:
recovered: true
errorMessage: ${ .caughtError.detail }
The SDK leaves ${ .caughtError } as null, so the expressions fail even though DSL semantics require the error object to be available.
We’d appreciate confirmation on whether these behaviors are intentional or if they should be treated as bugs so we can follow up accordingly.
We are analyzing the behavior of the Java SDK
TryExecutor.handleException()implementation (https://github.com/serverlessworkflow/sdk-java/blob/main/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java#L162C3-L197C4). The DSL spec states that an error must propagate when no catch filter/predicate matches the raised error, but the SDK currently returnsCompletableFuture.completedFuture(taskContext.rawOutput())before the filter is evaluated.Please clarify the intended design with these questions:
Unmatched filter flow
completableis initialized to a completed (successful) future before any error filter orwhen/exceptWhencheck. If the filter or predicate later evaluates tofalse, the method simply returns that success future instead of propagating the error. Is that intentional (design choice) or a bug? The DSL says the error should propagate (workflow should FAULT) when there is no match.whenandexceptWhenpredicatesSince the predicates short-circuit the condition, a
falsewhenortrueexceptWhenalso causes theifblock to be skipped. Yet the method still returns the success future created above. Shouldn’t the error be re-thrown whenwhen/exceptWhenreject the error, per the spec?Example: The error block raises a
503while thewhenpredicate only accepts status 400:Because the SDK returns the pre-resolved success future, the error never propagates even though
whenevaluated to false.catch.dofailure propagationThe same logic is reused when
catch.docontains nested try/catch. If an inner error is not matched, the innerhandleException()call returns the already-completed success future, so the catch.do chain never faulted even though the inner error should escape and eventually fault the workflow. Was this swallowing behavior intended?Example: An outer catch.do contains an inner try that raises an error but its filter rejects it:
The inner error type is
https://example.com/errors/inner, so the inner filter does not match. The SDK still returns the success future from the nestedhandleException(), meaning the outer try thinks catch.do succeeded rather than faulting.Error detail comparison
Can you confirm whether this line is a typo? https://github.com/serverlessworkflow/sdk-java/blob/main/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java#L210
Why this matters: in the current code the detail filter is compared to itself, so that check is always
truewhendetailsis present. This makescatch.errors.with.detailseffectively non-functional, because mismatched error details are never rejected.Example scenario:
Expected behavior (DSL): detail mismatch means catch filter does not match, so the error should propagate and the workflow should fault.
Current SDK behavior: catch is treated as matched and
catch.doruns, so the workflow completes successfully.catch.asbindingThe caught error object is never injected into the expression context for catch.do (no
taskContextor model mutation with theasvariable). Is that deliberate, or should the error be bound so${ .caughtError.detail }works for expressions?Example: A catch block binds the caught error to
caughtErrorand reads itsdetail:The SDK leaves
${ .caughtError }asnull, so the expressions fail even though DSL semantics require the error object to be available.We’d appreciate confirmation on whether these behaviors are intentional or if they should be treated as bugs so we can follow up accordingly.