Errors & retries

The error taxonomy, and how retries actually behave.

By default a task gets 3 attempts with exponential backoff starting at one second. Tune it per task:

export const syncContacts = task({
  id: 'sync-contacts',
  schema,
  attempts: 5,
  backoff: { type: 'exponential', delay: 2000 },
  run: async (payload) => { /* … */ },
});

A plain thrown error counts as retryable. For explicit control, use the error taxonomy:

import { NonRetryableError, RetryableError } from '@openqueue/sdk';

run: async (payload) => {
  const res = await fetch(url);

  if (res.status === 404) {
    // permanent — fail now, skip remaining attempts
    throw new NonRetryableError('contact list deleted');
  }
  if (res.status === 429) {
    // transient — retry with backoff
    throw new RetryableError('rate limited');
  }
};

The taxonomy

ErrorBehavior
RetryableErrorFails the attempt; retries until attempts is exhausted.
NonRetryableErrorFails the run immediately — no further attempts.
JobTimeoutErrorThrown by the runtime when a run exceeds the task's ttl.
JobExpiredErrorThe job sat in the queue past its expiry and was dropped.
JobCanceledErrorThe run was canceled (for example from Workbench).

Use isNonRetryable(err) when wrapping errors in your own code, and serializeError(err) if you need the same wire format OpenQueue stores on the run.

Triage in Workbench

Failed runs keep their payload, attempt history, log lines, and serialized error. The Errors view groups failures by error class and ranks them by frequency with a 24h trend — so a regression shows up as a spike, not as a support ticket. Retry a single run or a whole class from the UI.

On this page