Skip to main content
This reference documents all error types in Workflow DevKit, covering both workflow-level errors and runtime errors. Understanding these errors helps you build robust, production-ready workflows.

Error Categories

Workflow DevKit errors fall into two main categories:

Workflow Errors

Errors you can throw and handle within your workflow code to control execution flow:
  • FatalError - Stop retries and fail the step immediately
  • RetryableError - Customize retry timing and behavior

Runtime Errors

Errors thrown by the Workflow DevKit when internal operations fail:
  • WorkflowError - Base class for all Workflow DevKit errors
  • WorkflowAPIError - HTTP request failures
  • WorkflowRunFailedError - Workflow execution failures
  • WorkflowRunNotCompletedError - Accessing incomplete run results
  • WorkflowRuntimeError - Internal runtime issues
  • WorkflowRunNotFoundError - Missing workflow runs
  • WorkflowRunCancelledError - Cancelled workflow runs
  • RunNotSupportedError - Version incompatibility

Quick Start

Handling API Errors

import { WorkflowAPIError } from 'workflow';

try {
  await startWorkflow('myWorkflow', input);
} catch (error) {
  if (error instanceof WorkflowAPIError) {
    console.error(`API error (${error.status}):`, error.message);
    if (error.retryAfter) {
      console.log(`Retry after ${error.retryAfter} seconds`);
    }
  }
}

Using FatalError in Steps

import { FatalError } from 'workflow';

async function validateUser(userId: string) {
  "use step";
  
  const user = await fetchUser(userId);
  
  if (!user) {
    // Don't retry - user doesn't exist
    throw new FatalError('User not found');
  }
  
  return user;
}

Customizing Retry Timing

import { RetryableError } from 'workflow';

async function callRateLimitedAPI(endpoint: string) {
  "use step";
  
  const response = await fetch(endpoint);
  
  if (response.status === 429) {
    // Retry after 1 minute
    throw new RetryableError('Rate limited', {
      retryAfter: '1m'
    });
  }
  
  return response.json();
}

Error Documentation Pages

FatalError

Thrown to indicate an unrecoverable error that should not be retried. Use this when a step encounters a permanent failure condition like invalid input or missing resources.When to use:
  • Resource not found (404)
  • Invalid input validation
  • Permission denied (403)
  • Business logic violations

RetryableError

Thrown to customize retry behavior with specific timing. Use this when you know how long to wait before retrying (e.g., rate limits, backpressure).When to use:
  • Rate limiting (429)
  • Temporary service unavailability
  • Exponential backoff scenarios
  • Custom retry delays

WorkflowError

Base class for all Workflow DevKit errors. Use with instanceof to catch any Workflow-related error.

WorkflowAPIError

Thrown when HTTP requests to the Workflow backend fail due to network issues, invalid requests, or server errors.

WorkflowRunFailedError

Thrown when a workflow run encounters a fatal error during execution. Contains the underlying error details.

WorkflowRunNotCompletedError

Thrown when attempting to access results from a workflow that hasn’t completed yet.

WorkflowRuntimeError

Thrown when the runtime encounters internal errors like serialization failures or invalid workflow functions.

WorkflowRunNotFoundError

Thrown when attempting to access a workflow run that doesn’t exist.

WorkflowRunCancelledError

Thrown when attempting to get results from a cancelled workflow run.

RunNotSupportedError

Thrown when a workflow run requires a newer spec version than the current implementation supports.

Common Error Patterns

Catching Specific Error Types

import { 
  WorkflowAPIError, 
  WorkflowRunFailedError,
  WorkflowRunNotFoundError 
} from 'workflow';

try {
  const run = await getRun(runId);
  const result = await run.result;
} catch (error) {
  if (WorkflowRunNotFoundError.is(error)) {
    console.error('Run does not exist:', error.runId);
  } else if (WorkflowRunFailedError.is(error)) {
    console.error('Run failed:', error.cause.message);
  } else if (WorkflowAPIError.is(error)) {
    console.error('API error:', error.status, error.message);
  }
}

Handling Rate Limits

import { RetryableError, WorkflowAPIError } from 'workflow';

async function callAPI(endpoint: string) {
  "use step";
  
  try {
    const response = await fetch(endpoint);
    
    if (response.status === 429) {
      const retryAfter = response.headers.get('Retry-After');
      throw new RetryableError('Rate limited', {
        retryAfter: retryAfter ? `${retryAfter}s` : '60s'
      });
    }
    
    return response.json();
  } catch (error) {
    if (WorkflowAPIError.is(error) && error.retryAfter) {
      throw new RetryableError('API rate limited', {
        retryAfter: error.retryAfter * 1000 // Convert seconds to ms
      });
    }
    throw error;
  }
}

Exponential Backoff

import { RetryableError, getStepMetadata } from 'workflow';

async function unreliableOperation() {
  "use step";
  
  const metadata = getStepMetadata();
  
  try {
    return await performOperation();
  } catch (error) {
    // Exponential backoff: 1s, 4s, 16s, 64s...
    const delayMs = (metadata.attempt ** 2) * 1000;
    throw new RetryableError('Operation failed, retrying...', {
      retryAfter: delayMs
    });
  }
}

unreliableOperation.maxRetries = 5;

Best Practices

  1. Use FatalError for permanent failures - Don’t waste retries on errors that won’t succeed
  2. Use RetryableError for known delays - Respect rate limits and backpressure signals
  3. Check error types with .is() methods - Type-safe error checking works across contexts
  4. Set appropriate maxRetries - Default is 3, adjust based on your use case
  5. Make retry logic idempotent - Steps may execute multiple times
  6. Include context in error messages - Help debugging by providing relevant details

Build docs developers (and LLMs) love