# Error Handling

> Which HTTP status raises which class in each SDK, what an error carries, and which failures the SDKs retry before you see them.

Source: https://docs.levelfour.ai/error-handling


Every LevelFour SDK maps an HTTP status code to its own typed error class, and every one of those classes inherits from a base class you can catch when the specific type does not matter. Some failures never reach your handler at all, because the SDK retries them first.

## Error types

| HTTP Status | Error Code              | Python                | TypeScript            | Go                          |
| ----------- | ----------------------- | --------------------- | --------------------- | --------------------------- |
| 400         | `BAD_REQUEST`           | `BadRequestError`     | `BadRequestError`     | `*BadRequestError`          |
| 401         | `AUTHENTICATION_ERROR`  | `AuthenticationError` | `AuthenticationError` | `*UnauthorizedError`        |
| 403         | `AUTHORIZATION_ERROR`   | `AuthorizationError`  | `AuthorizationError`  | `*ForbiddenError`           |
| 404         | `NOT_FOUND`             | `NotFoundError`       | `NotFoundError`       | `*NotFoundError`            |
| 409         | `CONFLICT`              | `ConflictError`       | `ConflictError`       | `*ConflictError`            |
| 422         | `VALIDATION_ERROR`      | `ValidationError`     | `ValidationError`     | `*UnprocessableEntityError` |
| 429         | `RATE_LIMIT_EXCEEDED`   | `RateLimitError`      | `RateLimitError`      | `*TooManyRequestsError`     |
| 500         | `INTERNAL_SERVER_ERROR` | `InternalServerError` | `InternalServerError` | `*InternalServerError`      |

Each SDK's base class:

* Python: `LevelFourError`
* TypeScript: `LevelFourError`
* Go: `*core.APIError` (use `errors.As` to match specific types)

Python and TypeScript add two more, for failures that carry no HTTP status:

* Python: `LevelFourConnectionError`, `LevelFourTimeoutError`
* TypeScript: `LevelFourConnectionError`, `LevelFourTimeoutError`

> **Warning**
>
> In Python `status_code` is `0` on both, so a branch keyed on the status code will not catch a connection failure or a timeout. Catch the class by name.

## Catching errors

**Python**

```python
from levelfour import (
    LevelFour,
    LevelFourError,
    AuthenticationError,
    NotFoundError,
    RateLimitError,
    ValidationError,
)

client = LevelFour()

try:
    detail = client.recommendations.get("rec_123")
except NotFoundError as e:
    print(f"Not found: {e.message}")
    print(f"Status: {e.status_code}")
    print(f"Code: {e.code}")
except RateLimitError as e:
    print(f"Rate limited. Retry after: {e.retry_after}")
except ValidationError as e:
    print(f"Validation errors: {e.details}")
except AuthenticationError as e:
    print(f"Auth failed: {e.message}")
except LevelFourError as e:
    print(f"API error {e.status_code}: {e.message}")
```

**TypeScript**

```typescript
import {
    LevelFourClient,
    LevelFourError,
    NotFoundError,
    RateLimitError,
    ValidationError,
    AuthenticationError,
} from "levelfour";

const client = new LevelFourClient();

try {
    await client.recommendations.get({ recommendation_id: "rec_123" });
} catch (err) {
    if (err instanceof NotFoundError) {
        console.log("Not found:", err.message);
        console.log("Status:", err.statusCode);
    } else if (err instanceof RateLimitError) {
        console.log("Rate limited");
    } else if (err instanceof ValidationError) {
        console.log("Validation:", err.body);
    } else if (err instanceof AuthenticationError) {
        console.log("Auth failed:", err.message);
    } else if (err instanceof LevelFourError) {
        console.log(`API error ${err.statusCode}: ${err.message}`);
    }
}
```

**Go**

```go
import (
    "errors"
    "fmt"

    "github.com/LevelFourAI/levelfour-go/levelfour"
)

detail, err := client.Recommendations.Get(ctx, "rec_123")
if err != nil {
    var notFoundErr *levelfour.NotFoundError
    var rateLimitErr *levelfour.TooManyRequestsError
    var badReqErr *levelfour.BadRequestError
    var unprocessableErr *levelfour.UnprocessableEntityError

    switch {
    case errors.As(err, &notFoundErr):
        fmt.Printf("Not found: %v\n", notFoundErr.Body)
    case errors.As(err, &rateLimitErr):
        fmt.Printf("Rate limited: %v\n", rateLimitErr.Body)
    case errors.As(err, &badReqErr):
        fmt.Printf("Bad request: %v\n", badReqErr.Body)
    case errors.As(err, &unprocessableErr):
        fmt.Printf("Validation: %v\n", unprocessableErr.Body)
    default:
        fmt.Printf("Error: %v\n", err)
    }
}
```

> **Warning**
>
> Order the branches specific first, base last. In Python and TypeScript a branch on `LevelFourError` matches every class in the table above, so a specific branch below it never runs and you never read the fields that class adds.

## Error properties

**Python**

All exceptions inherit from `LevelFourError`:

| Property      | Type                   | Description                                        |
| ------------- | ---------------------- | -------------------------------------------------- |
| `status_code` | `int`                  | HTTP status code (0 for connection/timeout errors) |
| `code`        | `str`                  | Error code string                                  |
| `message`     | `str`                  | Human-readable error message                       |
| `details`     | `dict \| list \| None` | Additional error context                           |

`RateLimitError` adds:

| Property      | Type          | Description                    |
| ------------- | ------------- | ------------------------------ |
| `retry_after` | `str \| None` | Seconds until retry is allowed |

**TypeScript**

All error classes extend `LevelFourError`:

| Property      | Type                       | Description                |
| ------------- | -------------------------- | -------------------------- |
| `statusCode`  | `number \| undefined`      | HTTP status code           |
| `body`        | `unknown`                  | Parsed error response body |
| `rawResponse` | `RawResponse \| undefined` | Raw HTTP response          |
| `message`     | `string`                   | Error message              |

**Go**

All error types embed `*core.APIError`:

| Property     | Type  | Description                                   |
| ------------ | ----- | --------------------------------------------- |
| `StatusCode` | `int` | HTTP status code                              |
| `Body`       | typed | Parsed error response (type varies per error) |

The `Body` field is typed per error: `BadRequest`, `AuthenticationError`, `AuthorizationError`, `NotFound`, `Conflict`, `*HTTPValidationError`, `RateLimitError`, `InternalServer`.

## Retry behavior

Each SDK retries a failed request on its own, with exponential backoff. The default is two retries.

### Retryable errors

| Condition                 | Retried |
| ------------------------- | ------- |
| Network errors / timeouts | Yes     |
| 408 Request Timeout       | Yes     |
| 409 Conflict              | Yes     |
| 429 Too Many Requests     | Yes     |
| 5xx Server Errors         | Yes     |
| 400 Bad Request           | No      |
| 401 Unauthorized          | No      |
| 403 Forbidden             | No      |
| 404 Not Found             | No      |
| 422 Validation Error      | No      |

### Configuring retries

Set the retry count when you create the client. The first client in each example retries five times, and the second never retries.

**Python**

```python
client = LevelFour(max_retries=5)

client = LevelFour(max_retries=0)
```

**TypeScript**

```typescript
const client = new LevelFourClient({ maxRetries: 5 });

const client = new LevelFourClient({ maxRetries: 0 });
```

**Go**

```go
patient, _ := levelfour.NewClient("", levelfour.WithMaxRetries(5))

impatient, _ := levelfour.NewClient("", levelfour.WithNoRetries())
```

> **Tip**
>
> Turn retries off while you are debugging. A failure comes back at once instead of after two backoffs.

### Per-request override

**Python**

```python
summary = client.recommendations.get_savings_by_provider(
    request_options={"max_retries": 5},
)
```

**TypeScript**

```typescript
const summary = await client.recommendations.getSavingsByProvider({
    maxRetries: 5,
});
```

**Go**

```go
summary, err := client.Recommendations.GetSavingsByProvider(ctx,
    option.WithMaxAttempts(5),
)
```

Retries are one of several per-request options. Each SDK page has the full set: [Python](/sdks/python#request-options), [TypeScript](/sdks/typescript#request-options), [Go](/sdks/go#request-options).

## Rate limits

Each API key can make 600 requests a minute. Past that, the API answers `429` with the code `RATE_LIMIT_EXCEEDED`, and the SDKs back off and retry it as the [retryable errors](#retryable-errors) table shows. A batch job that walks many pages stays under the limit by paging sequentially rather than in parallel.

## Idempotent writes

Send an `Idempotency-Key` header on a `POST`, `PATCH` or `PUT` to make retrying it safe.

* The key must be a version 4 UUID. Anything else is a `422`.
* A successful response is kept for 24 hours. The same key from the same API key inside that window gets the stored response back, with an `Idempotent-Replayed: true` header, and the write does not run again.
* A failed request, `4xx` or `5xx`, is not kept, so retrying it with the same key runs it.
* A request whose key is still in flight gets `409`. Wait for the first one to finish.
* Keys belong to the caller that sent them. Two API keys using the same value do not collide.

```bash
curl -s -X POST "https://api.levelfour.ai/api/v1/webhooks/endpoints" \
  -H "Authorization: Bearer $LEVELFOUR_API_KEY" \
  -H "Idempotency-Key: $(uuidgen | tr 'A-Z' 'a-z')" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/webhook", "event_types": ["optimization.completed"]}'
```

Generate a new key for each distinct write, and reuse it only when retrying that same write.

## Next

* [SDKs](/sdks) compares the three clients and their constructor options, including the retry default
* [API Reference](/api-reference#response-format) is the JSON envelope these error codes arrive in over REST
* [Pagination](/pagination) is the other cross-SDK behavior, with auto-paginating iterators
