Error Handling
The host supplies a WorkflowErrorHandler to choose failure, retry, or recovery transitions. The default
handler fails. The combined branch adds structured diagnostics while retaining legacy methods and wire
fields; see current contracts for delivery status.
Structured and legacy callbacks
handleError(instance, node, result, WorkflowError error) is an additive default method. Override it to
inspect phase(), nodeId(), edgeId(), expression(), field(), message, and cause. IDs/fields that do
not apply are null. Existing handlers can continue implementing:
ErrorResolution handleNodeError(WorkflowInstance instance, WorkflowNode node,
NodeResult result, Exception error);
ErrorResolution handleNoMatchingEdge(WorkflowInstance instance, WorkflowNode node);
The default adapter preserves legacy dispatch. Explicit FAILED results supply the result and a null
exception. Executor exceptions preserve their original identity; newly diagnosed input/output/result
failures supply WorkflowError. Edge-condition exceptions carry the expression via
ConditionEvaluationException rather than being reduced to “no matching edge.” Missing-edge selection
still invokes handleNoMatchingEdge. Override structured handling for a uniform view across phases.
| Phase | Examples |
|---|---|
INPUT_RESOLUTION |
Action/human-task input expression threw before work or parking |
EXECUTOR_LOOKUP, EXECUTION |
Missing/throwing provider, thrown executor, explicit failed result |
RESULT_VALIDATION, OUTPUT_VALIDATION |
Null result/status, malformed output keys, missing required action output |
OUTPUT_MAPPING |
Receive-event mapping failed before merging |
EDGE_CONDITION, EDGE_SELECTION |
Condition exception or no matching edge |
ERROR_HANDLER |
Handler threw or returned an invalid resolution |
Recovery decisions
| Decision | Behavior |
|---|---|
ErrorResolution.fail() |
Set FAILED, preserve a diagnostic failureReason, notify onWorkflowFailed |
ErrorResolution.retry() |
Retry at the failed boundary; see the phase-specific behavior below |
ErrorResolution.transitionTo("repair") |
Enter an existing non-START node through normal execution/parking behavior |
Recovery affects the failed branch; unanswered parked siblings remain parked. Invalid output is not merged. A malformed handler fails once without recursive handler invocation, retaining the handler cause and original error. Failure of one branch fails the instance.
What RETRY repeats
| Failure boundary | RETRY behavior | Guard |
|---|---|---|
| Action input resolution, provider lookup, execution, result/required-output validation during execution | Repeat the action attempt from input resolution; executor side effects may repeat if execution is reached | Local action retry loop |
| Rejected external ACTION completion (result/required-output failure or FAILED result) | Queue action re-execution, resolving inputs again; do not merely await another completion | External retry consumes a driver unit; action re-execution has its own local retry loop |
| Non-action external completion or external output-mapping failure | Leave the instance parked for another host delivery; do not merge rejected output | No immediate retry loop |
| Human-task input failure on entry | Repeat input resolution before parking; this is not an external completion retry | Shared driver budget |
EDGE_CONDITION or EDGE_SELECTION |
Re-evaluate outgoing routing, without re-executing the completed action or awaiting another delivery | Unsuccessful selection consumes a shared driver unit |
Retry limits and durability
Each local action execution retry loop allows ten retries after its initial attempt. This is not a total allowance across every action in an engine call. Recovery entries, externally requested action retries, human input retries, ordinary edge moves, and unsuccessful edge selections use the shared 100-unit driver budget. Successful selection itself adds no charge; a fork charges each child move. These guards bound in-process loops; they do not schedule backoff or persist an attempt policy. A repeated immutable input error will not repair itself through immediate retries.
For transient action failures a handler can return retry() under its own host policy; for bad config or
data, transition to a repair node or fail. Do not mutate instance.context() to increment a retry counter:
it is an owned snapshot. Persist durable attempt counts and scheduling in the host. Declarative retry and
backoff remain proposed in #86.
Errors outside recovery
- Invalid definitions fail start with
WorkflowValidationException; required start input checks throw before execution. Inspect validation problems rather than treating this as an executor retry. - Completing a non-WAITING instance or a node that is not parked throws
IllegalStateException. - Read-only action/human info calls throw
WorkflowErroron input-resolution failure: no state can change. resolveExpressionthrowsConditionEvaluationException; event matching returns false on evaluation failure. Correlation does not complete the node.- Host JVM
Errors propagate. ListenerExceptions are logged/swallowed, and later listeners still run.
Host integration
failureReason remains a string for wire compatibility, not a stable machine-readable format.
onWorkflowFailed receives the structured error. Error recovery does not roll back external side effects;
use idempotency and durable host transactions/outboxes. Listeners are synchronous observations without
exactly-once delivery, replay, veto, or persistence guarantees.
See Engine errors and host extension contracts for registration validation, nullability, callback ordering, legacy adapter details, and migration guidance. The executable examples exercise these paths.