DOC_INDEX
THEME
DOCS/Learn OrchStep/Error Handling

Error Handling

retry with backoff, catch for recovery, finally for cleanup, and on_error policies

By default a failing step stops the task. Every loosening of that rule is explicit: retry (try again), catch (recover), finally (always clean up), on_error (downgrade the failure).

Retry with backoff

- name: flaky_call
  func: shell
  do: ./sometimes-fails.sh
  retry:
    max_attempts: 4        # total attempts, not extra retries
    interval: "200ms"      # delay before attempt 2
    backoff_rate: 2.0      # 200ms -> 400ms -> 800ms
    max_delay: "2s"        # cap the growth

These exact field names matter: max_attempts, interval, backoff_rate, max_delay. Variants like max:/delay:/backoff: are silently ignored - the step simply will not retry (verified against v0.7.1).

Two more verified behaviors: a when: condition can restrict which failures retry, and the engine prints a failure box for every failed attempt even when a later attempt succeeds - the run still exits 0.

Catch: recovery steps

- name: risky
  func: shell
  do: exit 1
  retry:
    max_attempts: 2
    interval: "50ms"
  catch:
    - name: rescue
      func: shell
      do: echo "recovering..."
  finally:
    - name: cleanup
      func: shell
      do: echo "always runs"

Order on failure: attempts exhaust -> catch steps run -> finally steps run. If catch succeeds, the workflow continues (the error is handled). finally runs on success too.

Protecting a whole task

Since v0.8.0, catch:/finally: attach directly to a task and run with the same semantics as the step level - catch recovers the task, finally always runs, and a failing catch keeps the task failed (verified):

tasks:
  pipeline:
    steps:
      - name: work
        func: shell
        do: ./might-fail.sh
    catch:
      - name: alert
        func: shell
        do: 'echo "ALERT: pipeline failed"'
    finally:
      - name: cleanup
        func: shell
        do: rm -rf ./tmp-workdir

The step-wrapper pattern (a step with task: plus catch/finally) still works and remains the way to protect a module task call, since module-internal task catch/finally is not yet wired:

- name: run_pipeline
  task: pipeline
  catch:
    - name: alert
      func: shell
      do: 'echo "ALERT: pipeline failed"'

on_error policies

- name: best_effort
  func: shell
  do: ./optional-step.sh
  on_error: ignore      # also: warn; default is fail

Run it

orchstep run pipeline

Where to go next

HTTP & APIs - the http function with retries makes robust API automation.