# How to Handle Errors in 9router API Routes: Patterns and Best Practices

> Master error handling in 9router API routes. Learn to create OpenAI-compatible JSON errors, normalize upstream failures, and manage rate limits with retry headers.

- Repository: [decolua/9router](https://github.com/decolua/9router)
- Tags: best-practices
- Published: 2026-05-08

---

**Use the centralized error utilities in [`open-sse/utils/error.js`](https://github.com/decolua/9router/blob/main/open-sse/utils/error.js) to return OpenAI-compatible JSON error responses, normalize upstream provider failures with `parseUpstreamError`, and signal rate-limit conditions via `unavailableResponse` with retry headers.**

The **decolua/9router** repository implements a consistent, standards-compliant error-handling strategy across all server-side API endpoints. This approach ensures that every route returns predictable JSON error objects while internal logic manages retries, logging, and provider-specific failure translation.

## Centralized Error Utilities

All route handlers in 9router rely on the helper functions exported from **[`open-sse/utils/error.js`](https://github.com/decolua/9router/blob/main/open-sse/utils/error.js)**. This module standardizes error formatting to match the OpenAI API schema, ensuring compatibility with existing client SDKs and downstream services.

### Core Error Helper Functions

The module provides six primary exports that cover every error scenario:

- **`buildErrorBody(statusCode, message)`** – Returns a plain object `{ error: { message, type, code } }` conforming to the OpenAI error schema.
- **`errorResponse(statusCode, message)`** – Wraps `buildErrorBody` in an HTTP `Response` with appropriate `Content-Type` and CORS headers.
- **`unavailableResponse(statusCode, message, retryAfter, retryAfterHuman)`** – Used when every credential for a provider is rate-limited; adds a `Retry-After` header to the response.
- **`parseUpstreamError(response, executor?)`** – Normalizes error payloads from upstream providers (OpenAI, Claude, Gemini, etc.), falling back to generic messages when provider formats differ.
- **`createErrorResult(statusCode, message, resetsAtMs?)`** – Returns a structured result object `{ success:false, status, error, response, … }` that core handlers return directly.
- **`formatProviderError(error, provider, model, statusCode)`** – Produces human-readable log strings that include low-level causes like `ECONNRESET` without exposing sensitive credentials.

*Source:* [[`open-sse/utils/error.js`](https://github.com/decolua/9router/blob/main/open-sse/utils/error.js)](https://github.com/decolua/9router/blob/master/open-sse/utils/error.js)

## Error Configuration and Type Mapping

The **[`open-sse/config/errorConfig.js`](https://github.com/decolua/9router/blob/main/open-sse/config/errorConfig.js)** file defines two critical lookup tables used by the error utilities:

- **`ERROR_TYPES`** – Maps HTTP status codes to OpenAI-style `type` and `code` values (e.g., 429 maps to `rate_limit_error` and `rate_limit_exceeded`).
- **`DEFAULT_ERROR_MESSAGES`** – Supplies friendly default messages when custom strings aren't provided, ensuring users always receive actionable feedback.
- **`ERROR_RULES`** – Flags specific error conditions with `backoff: true` to trigger exponential backoff calculations defined in `BACKOFF_CONFIG`.

*Source:* [[`open-sse/config/errorConfig.js`](https://github.com/decolua/9router/blob/main/open-sse/config/errorConfig.js)](https://github.com/decolua/9router/blob/master/open-sse/config/errorConfig.js)

## Standard Error Handling Flow in API Routes

The **[`src/sse/handlers/chat.js`](https://github.com/decolua/9router/blob/main/src/sse/handlers/chat.js)** file demonstrates the canonical five-step error handling pattern used across 9router routes.

### 1. Request Validation

Handlers immediately validate JSON payloads and required fields, returning `errorResponse` for malformed requests:

```javascript
import { errorResponse } from "open-sse/utils/error.js";
import { HTTP_STATUS } from "open-sse/config/runtimeConfig.js";

try { 
  body = await request.json(); 
} catch { 
  return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body"); 
}

if (!body.model) {
  return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing model");
}

```

*Source:* [[`src/sse/handlers/chat.js`](https://github.com/decolua/9router/blob/main/src/sse/handlers/chat.js)](https://github.com/decolua/9router/blob/master/src/sse/handlers/chat.js) (lines 30-35, 82-85)

### 2. Authentication Checks

Routes extract API keys via `extractApiKey` and validate them through `isValidApiKey`, returning `HTTP_STATUS.UNAUTHORIZED` for missing or invalid credentials:

```javascript
if (!apiKey) {
  return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key");
}

if (!(await isValidApiKey(apiKey))) {
  return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key");
}

```

*Source:* [[`src/sse/handlers/chat.js`](https://github.com/decolua/9router/blob/main/src/sse/handlers/chat.js)](https://github.com/decolua/9router/blob/master/src/sse/handlers/chat.js) (lines 68-79)

### 3. Provider Credential Selection

When retrieving credentials via `getProviderCredentials`, handlers check if all accounts are rate-limited. If so, they invoke `unavailableResponse` to communicate backoff timing to clients:

```javascript
if (!credentials || credentials.allRateLimited) {
  return unavailableResponse(
    status,
    `[${provider}/${model}] ${errorMsg}`,
    credentials.retryAfter,
    credentials.retryAfterHuman
  );
}

```

*Source:* [[`src/sse/handlers/chat.js`](https://github.com/decolua/9router/blob/main/src/sse/handlers/chat.js)](https://github.com/decolua/9router/blob/master/src/sse/handlers/chat.js) (lines 70-76)

### 4. Upstream Error Normalization

When forwarding requests to LLM providers, handlers use `parseUpstreamError` to translate provider-specific error formats into the standard OpenAI schema:

```javascript
const providerResponse = await fetch(providerUrl, providerOpts);
if (!providerResponse.ok) {
  const { statusCode, message } = await parseUpstreamError(providerResponse, executor);
  return createErrorResult(statusCode, message);
}

```

*Source:* [[`open-sse/handlers/chatCore.js`](https://github.com/decolua/9router/blob/main/open-sse/handlers/chatCore.js)](https://github.com/decolua/9router/blob/master/open-sse/handlers/chatCore.js) (around line 217)

## Reusable Patterns Across Route Handlers

Every API route in 9router imports the same error utilities, ensuring consistency across endpoints:

| Route | Validation Checks | Upstream Error Handling |
|-------|------------------|------------------------|
| [`chat.js`](https://github.com/decolua/9router/blob/main/chat.js) | JSON body, model, API key, credentials | `parseUpstreamError` → `createErrorResult` |
| [`tts.js`](https://github.com/decolua/9router/blob/main/tts.js) | JSON body, API key, model, input | `parseUpstreamError` → `errorResponse` |
| [`stt.js`](https://github.com/decolua/9router/blob/main/stt.js) | Multipart form, API key, model, file | Same pattern as [`tts.js`](https://github.com/decolua/9router/blob/main/tts.js) |
| [`imageGeneration.js`](https://github.com/decolua/9router/blob/main/imageGeneration.js) | JSON body, API key, model, prompt | Same pattern |
| [`fetch.js`](https://github.com/decolua/9router/blob/main/fetch.js) | URL validation, provider support | Same pattern |
| [`embeddings.js`](https://github.com/decolua/9router/blob/main/embeddings.js) | JSON body, model, input | Same pattern |

All handlers import:

```javascript
import { errorResponse, unavailableResponse } from "open-sse/utils/error.js";

```

*Source examples:* [[`src/sse/handlers/tts.js`](https://github.com/decolua/9router/blob/main/src/sse/handlers/tts.js)](https://github.com/decolua/9router/blob/master/src/sse/handlers/tts.js), [[`src/sse/handlers/imageGeneration.js`](https://github.com/decolua/9router/blob/main/src/sse/handlers/imageGeneration.js)](https://github.com/decolua/9router/blob/master/src/sse/handlers/imageGeneration.js)

## Rate Limiting and Retry Logic

When upstream providers return rate-limit errors, the credential manager (implemented in [`src/mitm/handlers/base.js`](https://github.com/decolua/9router/blob/main/src/mitm/handlers/base.js)) consults `ERROR_RULES` from [`errorConfig.js`](https://github.com/decolua/9router/blob/main/errorConfig.js) to calculate exponential backoff based on `BACKOFF_CONFIG`. The resulting `retryAfter` timestamp is passed to `unavailableResponse`, which sets the HTTP `Retry-After` header to inform clients when to retry.

## Logging and Error Diagnostics

Every error path logs concise, masked messages via **[`src/utils/logger.js`](https://github.com/decolua/9router/blob/main/src/utils/logger.js)**. The `formatProviderError` helper enriches these logs with low-level network causes (e.g., `ECONNRESET`, `ETIMEDOUT`) while ensuring API keys and tokens remain masked in log output.

*Source:* [[`open-sse/utils/error.js`](https://github.com/decolua/9router/blob/main/open-sse/utils/error.js)](https://github.com/decolua/9router/blob/master/open-sse/utils/error.js) (see `formatProviderError` around line 39)

## Practical Code Examples

### Returning a Validation Error

Handle missing required fields with standard OpenAI-compatible error responses:

```javascript
import { errorResponse } from "open-sse/utils/error.js";
import { HTTP_STATUS } from "open-sse/config/runtimeConfig.js";

export async function handleChat(request) {
  const body = await request.json();
  if (!body.model) {
    return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing model");
  }
  // ... processing continues
}

```

**Result:**

```json
{
  "error": {
    "message": "Missing model",
    "type": "invalid_request_error",
    "code": "bad_request"
  }
}

```

### Normalizing Upstream Provider Errors

Wrap provider-specific failures into the standard format:

```javascript
import { parseUpstreamError, createErrorResult } from "open-sse/utils/error.js";

const providerResponse = await fetch(providerUrl, options);
if (!providerResponse.ok) {
  const { statusCode, message } = await parseUpstreamError(providerResponse);
  return createErrorResult(statusCode, message);
}

```

**Result** (when provider returns 429):

```json
{
  "error": {
    "message": "Rate limit exceeded",
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded"
  }
}

```

### Handling Complete Service Unavailability

When all provider accounts are throttled, return a 503 with retry guidance:

```javascript
import { unavailableResponse } from "open-sse/utils/error.js";

if (credentials.allRateLimited) {
  return unavailableResponse(
    HTTP_STATUS.SERVICE_UNAVAILABLE,
    "[anthropic/claude] All accounts rate-limited",
    credentials.retryAfter,
    credentials.retryAfterHuman
  );
}

```

**HTTP Response Headers:**

```

Retry-After: 30

```

**Response Body:**

```json
{
  "error": {
    "message": "[anthropic/claude] All accounts rate-limited (reset after 30s)"
  }
}

```

## Summary

- **Centralize error handling** using [`open-sse/utils/error.js`](https://github.com/decolua/9router/blob/main/open-sse/utils/error.js) to ensure all routes return OpenAI-compatible JSON schemas.
- **Validate early** in route handlers using `errorResponse` for 400/401 errors before reaching upstream logic.
- **Normalize provider errors** with `parseUpstreamError` to abstract differences between OpenAI, Claude, and Gemini error formats.
- **Signal rate limits** via `unavailableResponse` to automatically communicate `Retry-After` headers to clients.
- **Log safely** using `formatProviderError` to capture low-level network errors without exposing credentials.

## Frequently Asked Questions

### What is the standard error response format in 9router?

9router returns OpenAI-compatible JSON error objects containing three fields: `message` (human-readable description), `type` (error category like `invalid_request_error`), and `code` (machine-readable identifier like `rate_limit_exceeded`). This format is generated by `buildErrorBody` in [`open-sse/utils/error.js`](https://github.com/decolua/9router/blob/main/open-sse/utils/error.js) and consumed by client SDKs expecting OpenAI API compatibility.

### How does 9router handle rate limit errors from upstream providers?

When an upstream provider returns a 429 status or when all configured credentials are exhausted, 9router invokes `unavailableResponse` with a calculated `retryAfter` value. This sets the HTTP `Retry-After` header and returns a 503 status code, allowing clients to implement intelligent backoff strategies automatically.

### Where are error utilities centralized in the 9router codebase?

All error handling utilities reside in **[`open-sse/utils/error.js`](https://github.com/decolua/9router/blob/main/open-sse/utils/error.js)**, with configuration constants defined in **[`open-sse/config/errorConfig.js`](https://github.com/decolua/9router/blob/main/open-sse/config/errorConfig.js)**. These modules provide `errorResponse`, `unavailableResponse`, `parseUpstreamError`, and `createErrorResult`, which are imported by every route handler including [`src/sse/handlers/chat.js`](https://github.com/decolua/9router/blob/main/src/sse/handlers/chat.js), [`tts.js`](https://github.com/decolua/9router/blob/main/tts.js), and [`imageGeneration.js`](https://github.com/decolua/9router/blob/main/imageGeneration.js).

### How can I add custom error handling to a new route in 9router?

Import the error utilities from [`open-sse/utils/error.js`](https://github.com/decolua/9router/blob/main/open-sse/utils/error.js), then wrap validation logic using `errorResponse` for client errors (400/401) and `parseUpstreamError` combined with `createErrorResult` for upstream failures. For availability issues, use `unavailableResponse` if you need to signal retry timing to clients. Always validate request bodies before authentication to fail fast on malformed requests.