Skip to content

Error Handling & Rollback (.onError)

Production workflows interact with unreliable external systems, rate-limited APIs, and bounded review loops that can fail mid-flight. AGBoss makes failure recovery explicit in HarnessDSL (HDSL) @Workflow({ flow }) chains via .onError(ErrorClass, sidePath) on individual steps and .onJoinError(ErrorClass, sidePath) on parallel joins, keeping compensation logic type-checked and unit-tested alongside the happy path.

How .onError() chains compensation steps in a workflow path

Section titled “How .onError() chains compensation steps in a workflow path”

Instead of hidden automatic rollbacks that guess how to undo a side effect, you attach .onError(ErrorClass, sidePath) directly after the @Action, @Agent, or @Router that can throw. The compensation side path has full Path Scope access to DTOs produced by earlier steps (such as a StockReservationDto needed to release reserved inventory):

What error classes can .onError() and .onJoinError() catch?

Section titled “What error classes can .onError() and .onJoinError() catch?”
Error Class Where It Attaches When It Fires
Custom Error subclass (e.g. CarrierError) .onError(CarrierError, ...) after @Action / @Router When the step throws CarrierError (or an ActionFailedError wrapping it in cause)
ChildExecutionFailedError .onError(ChildExecutionFailedError, ...) after .toWorkflow(Child) When a child workflow execution fails without reaching one of its @WorkflowFinish nodes
VisitsLimitExceededError .onError(VisitsLimitExceededError, ...) When an @Agent or @Router in a review loop exceeds its maxVisits ceiling
BudgetExceededError / StepsLimitExceededError / DepthLimitExceededError / LimitExceededError .onError(LimitExceededError, ...) When an execution or step limit is exceeded. Note ([graph.limit-handler-uses-model]): side paths catching limit errors may only run deterministic @Action and DecidedBy.Code @Router steps — invoking an @Agent or DecidedBy.Model @Router inside a limit handler is rejected at assembly time.
StepFailedError .onJoinError(StepFailedError, ...) after .join() When one or more parallel branches fail during .toParallelAgents() or .toParallelWorkflows()
QuorumNotReachedError .onJoinError(QuorumNotReachedError, ...) after .joinQuorum() / .joinQuorumGroups() When too many branches fail or the quorum deadline expires before the quorum is satisfied

How error matching and checkpoint recovery work

Section titled “How error matching and checkpoint recovery work”
  • Most-specific class matching: A step may declare multiple .onError(...) handlers; the framework matches the thrown error (and its .cause chain) against the most specific subclass in the prototype hierarchy. Duplicate handlers for the same error class on one step fail assembly ([flow.duplicate-error-handler]).
  • Durable across checkpoints: The failure record (lastError with error code, message, and failed node name) is persisted in workflow state so a side path that pauses for human intervention resumes cleanly.

How do declarative transactional sagas (compensateWith & committedBy) and automatic LIFO unwinding work?

Section titled “How do declarative transactional sagas (compensateWith & committedBy) and automatic LIFO unwinding work?”

Manual .onError(...) chains only catch step failures along normal graph edges — they do not run when an execution or parallel branch is externally aborted by app.cancel(threadId), sibling FailurePolicy.FailFast, quorum BranchVerdict.Surplus, or a limit error (BudgetExceededError, StepsLimitExceededError, DepthLimitExceededError).

AGBoss solves both static rollback completeness and runtime external aborts with declarative compensation contracts on @Action:

  1. Declarative @Action saga options:
    • compensateWith: WithdrawOfferAction declares the deterministic @Action that reverses this action’s external side effect. Its input must be satisfiable from the Path Scope immediately after the compensated action completes (verified at assembly; otherwise fails with [action.invalid-compensator-input]).
    • committedBy: ConfirmOfferAction (optional) marks the commit boundary: once ConfirmOfferAction completes, the earlier action is finalized and will no longer be rolled back on subsequent errors or cancellations.
  2. Static saga verification in agboss check ([flow.uncompensated-action]):
    • Every error path (.onError / .onJoinError) after an uncommitted compensatable @Action that reaches a @WorkflowFinish before passing through committedBy must execute compensateWith on that path. If any error branch bypasses the compensator, agboss check fails assembly with [flow.uncompensated-action].
    • agboss describe and agboss describe --json include compensateWith and committedBy metadata on action nodes.
  3. Automatic runtime LIFO unwinding on external abort:
    • Whenever an @Action with compensateWith completes, the runtime checkpoints an uncommitted compensation entry (stepId, action, outputDto, compensator).
    • If the execution or branch is aborted before committedBy (via app.cancel(threadId), FailurePolicy.FailFast, quorum Surplus, or BudgetExceededError), the runtime automatically executes all uncommitted compensators in reverse completion order (LIFO) using a live cleanup signal bounded by @Application({ compensationTimeout: seconds(15) }).
    • If a worker pod crashes mid-rollback, the recovering instance resumes the remaining uncompensated entries idempotently using deterministic step.idempotencyKey values.

Inspect 04-travel-booking-saga and 02-path-scope-proof (cases/04-uncompensated-action) in agbossjs/agboss-examples for runnable proofs of reverse-order LIFO compensation across payment-saga, budget-saga, quorum-saga, and hitl-hold-saga, idempotent external ledgers (step.idempotencyKey), and crash-safe SQLite WAL recovery (see 7 Verified Examples).