Skip to content

Error Handling

How errors work by default

When a step throws an error, the platform retries it automatically according to its retry configuration. After exhausting all retries, the error propagates up the call stack. If it reaches the top-level run() method, the workflow run is marked errored and halted.

Configuring retries on a step

Open any node's config drawer and set the retry fields:

FieldDefaultDescription
Retry Limit5Maximum number of retry attempts
Retry Delay10 secondsDelay between retries
Backoffconstantconstant, linear, or exponential
TimeoutMax duration for a single attempt

Generated retry config

typescript
const fetch_data = await step.do(
  'Fetch Data',
  {
    retries: {
      limit: 3,
      delay: '5 seconds',
      backoff: 'exponential',
    },
    timeout: '30 seconds',
  },
  async () => {
    const res = await fetch('https://api.example.com/data')
    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    return await res.json()
  },
)

Try/Catch node

The Try/Catch node wraps a set of steps in a try/catch/finally block. Use it when you want to handle errors without stopping the entire workflow.

Structure

BranchDescription
trySteps to attempt
catchSteps to run if any step in the try branch throws
finallySteps that always run, regardless of success or failure

Generated code

typescript
try {
  const charge = await step.do('Charge Card', async () => {
    // ...
  })
  await step.do('Send Receipt', async () => {
    // ...
  })
} catch (err) {
  await step.do('Refund and Notify', async () => {
    // log the error, notify ops team, etc.
  })
} finally {
  await step.do('Release Lock', async () => {
    // always clean up
  })
}

TIP

The err variable inside the catch branch is available as err.message in expressions. Reference it with {{try_catch_node.error}} — the exact path depends on what the catch step returns.

NonRetryableError

Some errors should not be retried — for example, a 400 Bad Request from an API means the request itself is invalid and retrying will not help. Throw NonRetryableError to skip all remaining retry attempts and fail immediately.

typescript
import { NonRetryableError } from 'cloudflare:workers'

const result = await step.do('Call API', async () => {
  const res = await fetch('https://api.example.com/submit', {
    method: 'POST',
    body: JSON.stringify({ data }),
  })

  if (res.status === 400) {
    const body = await res.json()
    throw new NonRetryableError(`Invalid request: ${body.message}`)
  }

  if (!res.ok) {
    // 5xx errors: retryable
    throw new Error(`Server error: ${res.status}`)
  }

  return await res.json()
})

WARNING

NonRetryableError must be imported from cloudflare:workers. It is available inside any Step node's code editor — the import is injected automatically by the code generator.

Error propagation summary

ScenarioBehavior
Step throws a retryable errorPlatform retries up to the retry limit
Step throws NonRetryableErrorFails immediately, no retries
Step exhausts retriesError propagates to parent (Try/Catch or top level)
Try/Catch catch branch throwsError propagates to the next outer Try/Catch or top level
Uncaught error at top levelRun is marked errored