Errors & Retrying

By default, errors thrown inside steps are retried. Additionally, Workflow DevKit provides two new types of errors you can use to customize retries.


Default Retrying

By default, steps retry up to 3 times on arbitrary errors. You can customize the number of retries by adding a maxRetries property to the step function

async function callApi(endpoint: string) {
  "use step";

  const response = await fetch(endpoint);

  if (response.status >= 500) {
    // Any uncaught error gets retried
    throw new Error("Uncaught exceptions get retried!"); 
  }

  return response.json();
}

callApi.maxRetries = 5; // Set a custom number of retries

Steps get enqueued immediately after the failure. Read on to see how this can be customized.

Intentional Errors

When your step needs to intentionally throw an error and skip retrying, simply throw a FatalError.

import { FatalError } from 'workflow';

async function callApi(endpoint: string) {
  "use step";

  const response = await fetch(endpoint);

  if (response.status >= 500) {
    // Any uncaught error gets retried
    throw new Error("Uncaught exceptions get retried!");
  }

  if (response.status === 404) {
    throw new FatalError("Resource not found. Skipping retries."); 
  }

  return response.json();
}

Customize Retry Behavior

When you need to customize the delay on the retry, use RetryableErrorand set the retryAfter property.

import { FatalError, RetryableError } from 'workflow';

async function callApi(endpoint: string) {
  "use step";

  const response = await fetch(endpoint);

  if (response.status >= 500) {
    throw new Error("Uncaught exceptions get retried!");
  }

  if (response.status === 404) {
    throw new FatalError("Resource not found. Skipping retries.");
  }

  if (response.status === 429) {
    const retryAfter = response.headers.get('Retry-After');
    // Delay the retry until after a timeout
    throw new RetryableError("Too many requests. Retrying...", { 
      retryAfter: parseInt(retryAfter) 
    }); 
  }

  return response.json();
}

Advanced Example

This final example combines everything we've learnt, along with getStepMetadata.

import { FatalError, RetryableError, getStepMetadata } from 'workflow';

async function callApi(endpoint: string) {
  "use step";

  const metadata = getStepMetadata();

  const response = await fetch(endpoint);

  if (response.status >= 500) {
    // Exponential backoffs
    throw new RetryableError("Backing off...", { retryAfter: metadata.attempt ** 2 }); 
  }

  if (response.status === 404) {
    throw new FatalError("Resource not found. Skipping retries.");
  }

  if (response.status === 429) {
    const retryAfter = response.headers.get('Retry-After');
    // Delay the retry until after a timeout
    throw new RetryableError("Too many requests. Retrying...", {
      retryAfter: parseInt(retryAfter)
    });
  }

  return response.json();
}
callApi.maxRetries = 5;