Skip to content

Error Handling

There are three ways to handle errors, each for a different purpose. Most workflows combine all three.

1. Recover inside the step

Use try/catch when you can handle the error and continue:

typescript
.step('fetch', async ({ input }) => {
  try {
    return await callAPI(input.url)
  } catch {
    return { data: null, failed: true }
  }
})

2. Retry transient failures

Let the error throw and configure retry:

typescript
.step('charge', {
  retry: { maxAttempts: 3, backoff: 'exponential' },
  handler: async ({ input }) => await stripe.charge(input.amount),
})

3. Compensate with onFailure

Roll back after the run has failed — the saga pattern. See Failure Handling:

typescript
.onFailure(async ({ error, stepName, input }) => {
  if (stepName === 'charge') await refundAccount(input.userId)
})

The error hierarchy

Every error Reflow throws extends ReflowError, so a single instanceof check catches them all. Subclasses carry structured context — no message parsing needed.

typescript
import { ReflowError, WorkflowNotFoundError, ValidationError } from 'reflow-ts'

try {
  await engine.enqueue('nonexistent', {})
} catch (error) {
  if (error instanceof WorkflowNotFoundError) {
    console.log(error.workflowName) // 'nonexistent'
  }
  if (error instanceof ValidationError) {
    console.log(error.issues) // [{ message: '…', path: [...] }]
  }
  if (error instanceof ReflowError) {
    // catch-all for any Reflow error
  }
}

In hooks you can branch on the failure type:

typescript
import { StepTimeoutError } from 'reflow-ts'

hooks: {
  onRunFailed: ({ error }) => {
    if (error instanceof StepTimeoutError) {
      console.log(`Timed out after ${error.timeoutMs}ms`)
    }
  },
}

Error classes

ErrorThrown whenStructured properties
ReflowErrorBase class for all errors
ConfigErrorInvalid engine, retry, or schedule config
WorkflowNotFoundErrorenqueue() / schedule() with an unknown nameworkflowName
DuplicateWorkflowErrorSame workflow registered twiceworkflowName
DuplicateStepError.step() / .parallel() reuses a nameworkflowName, stepName
ParallelCompleteErrorcomplete() called inside a parallel branchstepName
ValidationErrorInput fails schema validationissues
IdempotencyConflictErrorSame idempotency key with different inputworkflowName, idempotencyKey
SerializationErrorStep output contains non-JSON data (NaN, functions, …)path
StepTimeoutErrorStep exceeds timeoutMstimeoutMs
RunCancelledErrorRun cancelled via engine.cancel()runId
LeaseExpiredErrorWorker lost its lease on a runrunId

See the Errors API reference for the full hierarchy.

Released under the MIT License.