Error Handling

Error Handling

Handle API errors gracefully with proper retry logic and user feedback. Learn the error codes, when to retry, and how to build resilient integrations.

Error Response Format

All API errors return a consistent JSON structure:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [
      { "field": "price", "reason": "Must be a positive number" },
      { "field": "slug", "reason": "Already exists" }
    ]
  }
}
FieldTypeDescription
codestringMachine-readable error code
messagestringHuman-readable description
detailsarray?Field-level errors (for validation)

Error Codes

Client Errors (4xx)

StatusCodeDescriptionRetry?
400BAD_REQUESTMalformed request❌ Fix request
400VALIDATION_ERRORField validation failed❌ Fix fields
401UNAUTHORIZEDMissing/invalid API key❌ Check key
403FORBIDDENInsufficient permissions❌ Check scopes
404NOT_FOUNDResource doesn't exist❌ Check ID
409CONFLICTDuplicate data❌ Resolve conflict
429RATE_LIMITEDToo many requests✅ With backoff

Server Errors (5xx)

StatusCodeDescriptionRetry?
500INTERNAL_ERRORServer error✅ With backoff
503SERVICE_UNAVAILABLETemporarily unavailable✅ With backoff

Retry Strategy

Use exponential backoff with jitter for retryable errors:

async function fetchWithRetry(url, options, maxRetries = 3) {
  let lastError;
 
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);
 
      // Don't retry client errors (except 429)
      if (response.status >= 400 && response.status < 500 && response.status !== 429) {
        const error = await response.json();
        throw new APIError(response.status, error);
      }
 
      // Retry on 429 or 5xx
      if (response.status === 429 || response.status >= 500) {
        throw new RetryableError(response.status);
      }
 
      return response;
    } catch (error) {
      lastError = error;
 
      if (error instanceof APIError) throw error;
 
      if (attempt < maxRetries - 1) {
        // Exponential backoff: 1s, 2s, 4s + jitter
        const delay = Math.pow(2, attempt) * 1000;
        const jitter = Math.random() * 1000;
        await sleep(delay + jitter);
      }
    }
  }
 
  throw lastError;
}

Retry Decision Chart

StatusRetry?Action
400, 401, 403, 404, 409Fix the issue first
429Retry with backoff
500, 502, 503, 504Retry with exponential backoff
Network errorRetry with exponential backoff

Handling Validation Errors

Validation errors include field-level details:

async function createProduct(data) {
  const response = await fetch('/stores/{storeId}/products', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
  });
 
  if (!response.ok) {
    const { error } = await response.json();
 
    if (error.code === 'VALIDATION_ERROR' && error.details) {
      // Map errors to form fields
      const fieldErrors = {};
      for (const detail of error.details) {
        fieldErrors[detail.field] = detail.reason;
      }
      return { success: false, fieldErrors };
    }
 
    throw new Error(error.message);
  }
 
  return { success: true, data: await response.json() };
}

Graceful Degradation

Build resilient UIs that handle failures gracefully:

function ProductList() {
  const [products, setProducts] = useState([]);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    async function loadProducts() {
      try {
        const response = await fetchWithRetry('/stores/{storeId}/products');
        const data = await response.json();
        setProducts(data.items);
        // Cache for fallback
        localStorage.setItem('products_cache', JSON.stringify(data.items));
      } catch (err) {
        setError(err);
        // Load from cache
        const cached = localStorage.getItem('products_cache');
        if (cached) setProducts(JSON.parse(cached));
      }
    }
    loadProducts();
  }, []);
 
  if (error && products.length === 0) {
    return <ErrorState message="Unable to load products" />;
  }
 
  return (
    <>
      {error && <Banner>Showing cached data</Banner>}
      <ProductGrid products={products} />
    </>
  );
}

Logging Errors

Log errors with context for debugging:

async function apiRequest(endpoint, options) {
  const requestId = generateRequestId();
 
  try {
    const response = await fetch(endpoint, options);
 
    if (!response.ok) {
      const error = await response.json();
 
      console.error('API Error', {
        requestId,
        endpoint,
        status: response.status,
        code: error.error?.code,
        message: error.error?.message,
      });
 
      throw new APIError(response.status, error);
    }
 
    return response;
  } catch (err) {
    console.error('Network Error', { requestId, endpoint, error: err.message });
    throw err;
  }
}

Best Practices

  1. Check error codes, not just status — Use error.code for precise handling
  2. Implement exponential backoff — Add jitter to prevent thundering herd
  3. Show user-friendly messages — Don't expose raw API errors
  4. Log errors with context — Include request ID, endpoint, and timing
  5. Cache for graceful degradation — Show stale data rather than errors

Related Guides