> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/vercel/workflow/llms.txt
> Use this file to discover all available pages before exploring further.

# Workflow Errors

> Control step execution with FatalError and RetryableError.

Workflow errors are error types you can throw within your workflow and step functions to control execution flow and retry behavior. These errors give you fine-grained control over how failures are handled.

## FatalError

A fatal error stops retries immediately and fails the step. Use this when you encounter a permanent failure condition that won't succeed on retry.

### When to Use FatalError

* **Resource not found** - API returns 404
* **Invalid input** - Validation failures
* **Permission denied** - Authentication/authorization errors (401, 403)
* **Business logic violations** - Insufficient funds, duplicate orders
* **Configuration errors** - Missing required environment variables

### Basic Usage

```typescript theme={null}
import { FatalError } from 'workflow';

async function getUser(userId: string) {
  "use step";
  
  const response = await fetch(`https://api.example.com/users/${userId}`);
  
  if (response.status === 404) {
    throw new FatalError('User not found');
  }
  
  return response.json();
}
```

### API Reference

<Accordion title="FatalError Constructor">
  ```typescript theme={null}
  new FatalError(message: string)
  ```

  **Parameters:**

  * `message` (string) - Error message describing the failure

  **Properties:**

  * `name` - Always `"FatalError"`
  * `message` - The error message
  * `fatal` - Always `true`
  * `stack` - Stack trace

  **Methods:**

  * `FatalError.is(value)` - Type guard to check if value is a FatalError
</Accordion>

### Examples

<Accordion title="Validation Errors">
  ```typescript theme={null}
  import { FatalError } from 'workflow';

  interface OrderInput {
    userId: string;
    items: string[];
    total: number;
  }

  async function createOrder(input: OrderInput) {
    "use step";
    
    // Validate input
    if (!input.userId || input.userId.trim() === '') {
      throw new FatalError('userId is required');
    }
    
    if (!input.items || input.items.length === 0) {
      throw new FatalError('Order must contain at least one item');
    }
    
    if (input.total <= 0) {
      throw new FatalError('Order total must be greater than zero');
    }
    
    // Create order...
    const order = await saveOrder(input);
    return order;
  }
  ```
</Accordion>

<Accordion title="Resource Not Found">
  ```typescript theme={null}
  import { FatalError } from 'workflow';

  async function processDocument(documentId: string) {
    "use step";
    
    const response = await fetch(
      `https://api.example.com/documents/${documentId}`
    );
    
    if (response.status === 404) {
      throw new FatalError(`Document ${documentId} not found`);
    }
    
    if (response.status === 403) {
      throw new FatalError('Permission denied to access document');
    }
    
    return response.json();
  }
  ```
</Accordion>

<Accordion title="Business Logic Violations">
  ```typescript theme={null}
  import { FatalError } from 'workflow';

  async function chargePayment(userId: string, amount: number) {
    "use step";
    
    const account = await getAccount(userId);
    
    if (account.balance < amount) {
      throw new FatalError(
        `Insufficient funds. Balance: ${account.balance}, Required: ${amount}`
      );
    }
    
    // Check for duplicate transaction
    const existingCharge = await findExistingCharge(userId, amount);
    if (existingCharge) {
      throw new FatalError('Duplicate charge detected');
    }
    
    return await processCharge(account, amount);
  }
  ```
</Accordion>

<Accordion title="Type Checking">
  ```typescript theme={null}
  import { FatalError } from 'workflow';

  export async function processWorkflow(input: unknown) {
    "use workflow";
    
    try {
      await riskyStep();
    } catch (error) {
      if (FatalError.is(error)) {
        // This is a fatal error - don't retry the workflow
        console.error('Fatal error occurred:', error.message);
        throw error;
      }
      // Other errors - might be retryable
      console.warn('Retryable error:', error);
      throw error;
    }
  }
  ```
</Accordion>

## RetryableError

A retryable error allows you to customize when a step should be retried. Use this when you know how long to wait before the operation might succeed.

### When to Use RetryableError

* **Rate limiting** - API returns 429 with Retry-After header
* **Temporary unavailability** - Service is temporarily down (503)
* **Backpressure** - System is overloaded but will recover
* **Exponential backoff** - Progressive retry delays
* **Scheduled retries** - Retry at a specific time

### Basic Usage

```typescript theme={null}
import { RetryableError } from 'workflow';

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

### API Reference

<Accordion title="RetryableError Constructor">
  ```typescript theme={null}
  new RetryableError(message: string, options?: RetryableErrorOptions)
  ```

  **Parameters:**

  * `message` (string) - Error message describing the failure
  * `options` (object, optional) - Retry configuration
    * `retryAfter` (number | string | Date, optional) - When to retry
      * Number: milliseconds
      * String: duration (e.g., `"5s"`, `"2m"`, `"1h"`)
      * Date: specific time to retry
      * Default: 1 second (1000ms)

  **Properties:**

  * `name` - Always `"RetryableError"`
  * `message` - The error message
  * `retryAfter` - Date when the step should be retried
  * `stack` - Stack trace

  **Methods:**

  * `RetryableError.is(value)` - Type guard to check if value is a RetryableError
</Accordion>

### Examples

<Accordion title="Rate Limit Handling">
  ```typescript theme={null}
  import { RetryableError } from 'workflow';

  async function callRateLimitedAPI(endpoint: string) {
    "use step";
    
    const response = await fetch(endpoint);
    
    if (response.status === 429) {
      // Check for Retry-After header
      const retryAfter = response.headers.get('Retry-After');
      
      if (retryAfter) {
        // Retry-After can be seconds or HTTP date
        const retrySeconds = parseInt(retryAfter, 10);
        
        if (!isNaN(retrySeconds)) {
          // Retry after N seconds
          throw new RetryableError('Rate limited by API', {
            retryAfter: retrySeconds * 1000 // Convert to ms
          });
        } else {
          // Retry at specific date
          throw new RetryableError('Rate limited by API', {
            retryAfter: new Date(retryAfter)
          });
        }
      }
      
      // Default retry after 1 minute
      throw new RetryableError('Rate limited by API', {
        retryAfter: '1m'
      });
    }
    
    return response.json();
  }
  ```
</Accordion>

<Accordion title="Exponential Backoff">
  ```typescript theme={null}
  import { RetryableError, getStepMetadata } from 'workflow';

  async function unreliableOperation() {
    "use step";
    
    const metadata = getStepMetadata();
    
    try {
      const response = await fetch('https://api.example.com/data');
      
      if (!response.ok) {
        throw new Error('Request failed');
      }
      
      return response.json();
    } catch (error) {
      // Exponential backoff: 1s, 4s, 16s, 64s, 256s
      const delayMs = (metadata.attempt ** 2) * 1000;
      
      throw new RetryableError(
        `Attempt ${metadata.attempt} failed, retrying...`,
        { retryAfter: delayMs }
      );
    }
  }

  // Allow up to 5 retries (6 total attempts)
  unreliableOperation.maxRetries = 5;
  ```
</Accordion>

<Accordion title="Service Unavailable">
  ```typescript theme={null}
  import { RetryableError, FatalError } from 'workflow';

  async function callExternalService(endpoint: string) {
    "use step";
    
    const response = await fetch(endpoint);
    
    if (response.status === 503) {
      // Service temporarily unavailable
      throw new RetryableError('Service unavailable', {
        retryAfter: '30s'
      });
    }
    
    if (response.status === 500) {
      // Server error - might recover
      throw new RetryableError('Server error', {
        retryAfter: '10s'
      });
    }
    
    if (response.status === 404) {
      // Permanent failure
      throw new FatalError('Endpoint not found');
    }
    
    return response.json();
  }
  ```
</Accordion>

<Accordion title="Scheduled Retry">
  ```typescript theme={null}
  import { RetryableError } from 'workflow';

  async function checkBusinessHours() {
    "use step";
    
    const now = new Date();
    const hour = now.getHours();
    
    // Business hours: 9 AM - 5 PM
    if (hour < 9 || hour >= 17) {
      // Calculate next 9 AM
      const tomorrow = new Date(now);
      tomorrow.setDate(tomorrow.getDate() + 1);
      tomorrow.setHours(9, 0, 0, 0);
      
      throw new RetryableError('Outside business hours', {
        retryAfter: tomorrow
      });
    }
    
    // Continue processing during business hours
    return await processBusinessLogic();
  }
  ```
</Accordion>

<Accordion title="Duration String Formats">
  ```typescript theme={null}
  import { RetryableError } from 'workflow';

  // All valid duration formats:
  throw new RetryableError('Retry needed', {
    retryAfter: '5s'    // 5 seconds
  });

  throw new RetryableError('Retry needed', {
    retryAfter: '2m'    // 2 minutes
  });

  throw new RetryableError('Retry needed', {
    retryAfter: '1h'    // 1 hour
  });

  throw new RetryableError('Retry needed', {
    retryAfter: '24h'   // 24 hours
  });

  throw new RetryableError('Retry needed', {
    retryAfter: 5000    // 5000 milliseconds (5 seconds)
  });

  throw new RetryableError('Retry needed', {
    retryAfter: new Date('2026-03-04T10:00:00Z') // Specific time
  });
  ```
</Accordion>

## Combining FatalError and RetryableError

Use both error types together to handle different failure scenarios appropriately:

```typescript theme={null}
import { FatalError, RetryableError, getStepMetadata } from 'workflow';

async function robustAPICall(endpoint: string) {
  "use step";
  
  const metadata = getStepMetadata();
  
  try {
    const response = await fetch(endpoint);
    
    // Permanent failures - don't retry
    if (response.status === 400) {
      throw new FatalError('Bad request - invalid input');
    }
    if (response.status === 401) {
      throw new FatalError('Unauthorized - check credentials');
    }
    if (response.status === 403) {
      throw new FatalError('Forbidden - insufficient permissions');
    }
    if (response.status === 404) {
      throw new FatalError('Resource not found');
    }
    
    // Temporary failures - retry with backoff
    if (response.status === 429) {
      const retryAfter = response.headers.get('Retry-After');
      throw new RetryableError('Rate limited', {
        retryAfter: retryAfter ? `${retryAfter}s` : '60s'
      });
    }
    
    if (response.status >= 500) {
      // Exponential backoff for server errors
      throw new RetryableError('Server error', {
        retryAfter: (metadata.attempt ** 2) * 1000
      });
    }
    
    return response.json();
  } catch (error) {
    // Network errors are retryable
    if (error instanceof TypeError && error.message.includes('fetch')) {
      throw new RetryableError('Network error', {
        retryAfter: '5s'
      });
    }
    
    // Re-throw FatalError and RetryableError as-is
    throw error;
  }
}

robustAPICall.maxRetries = 5;
```

## Best Practices

1. **Use FatalError for permanent failures**
   * Don't waste retries on errors that will never succeed
   * Examples: validation errors, 404s, permission errors

2. **Use RetryableError for temporary failures**
   * Respect rate limits and backpressure signals
   * Use appropriate delays based on the error type

3. **Set appropriate maxRetries**
   ```typescript theme={null}
   // For operations that should retry aggressively
   importantOperation.maxRetries = 10;

   // For operations that should fail fast
   quickOperation.maxRetries = 1;

   // To disable retries entirely
   noRetryOperation.maxRetries = 0;
   ```

4. **Implement exponential backoff**
   * Use `getStepMetadata()` to access attempt number
   * Increase delay with each attempt

5. **Make steps idempotent**
   * Steps may execute multiple times
   * Ensure repeated execution is safe
   * See [Idempotency Guide](/concepts/idempotency)

6. **Include helpful error messages**
   ```typescript theme={null}
   // Good - includes context
   throw new FatalError(`User ${userId} not found in database`);

   // Bad - lacks context
   throw new FatalError('Not found');
   ```

## Related Resources

* [Errors & Retrying Guide](/concepts/errors-and-retries)
* [Error Reference Overview](/errors/overview)
* [Runtime Errors Reference](/errors/runtime-errors)
* [Step Metadata API](/api-reference/workflow/get-step-metadata)
* [Idempotency Guide](/concepts/idempotency)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.