# Error handling

> OpenWAFError carries the HTTP status, the API error code, the field and the decoded body; how to handle version conflicts and client-side failures.

Every API error, an answer with a status of 400 or more, is thrown (TypeScript) or raised (Python) as `OpenWAFError`:

| Property | Meaning |
|---|---|
| `status` | HTTP status. `0` when the request got no answer from the API (see below). |
| `code` | The API's error code, such as `not_found`, `invalid`, `forbidden`, `reason_required` or `version_conflict`. `http_<status>` when the answer has no code. |
| `message` | The API's message. |
| `field` | For validation errors, the field the API names, when it does. |
| `body` | The decoded error body, with any extra data such as `current_version` or `hostnames`. |

The codes are the API's; the ones specific to versioned writes are listed in [Change control errors](https://developers.openwaf.dev/change-control/errors/).

## Version conflicts

A write sent with an expected version that is no longer current fails with status `412` and code `version_conflict`, and applies nothing. `body.current_version` is the zone's version now. Simulate again, review the new plan and confirm with the new version; do not retry blindly.

**TypeScript**

```ts
import { OpenWAFError } from "./openwaf.ts";

try {
  await owf.createRecord({ zone: zoneId, body: record }, { reason, ifMatch: plan.from_version! });
} catch (err) {
  if (err instanceof OpenWAFError && err.code === "version_conflict") {
    const { current_version } = err.body as { current_version: number };
    console.log(`zone is now at version ${current_version}: simulate again`);
  } else {
    throw err;
  }
}
```
**Python**

```py
from openwaf import OpenWAFError

try:
    owf.create_record(zone=zone_id, body=record, reason=reason, if_match=plan["from_version"])
except OpenWAFError as e:
    if e.code == "version_conflict":
        print("zone is now at version %d: simulate again" % e.body["current_version"])
    else:
        raise
```
`str(e)` reads `version_conflict (HTTP 412): the configuration changed: the current version is 9`.


## Validation errors

`invalid` (status 422) means the API rejected a value; `field` names it when it can, such as `content` or `records[0].ttl`. A simulation fails the same way as the real write, so a dry run is a safe way to validate input.

## Client-side failures

| Failure | TypeScript | Python |
|---|---|---|
| No token configured | `OpenWAFError`, status 0, code `no_token` | Same |
| The organisation cannot be worked out (pass `org`) | `OpenWAFError`, status 0, code `org_unresolved` | Same |
| No `fetch` available | `OpenWAFError`, status 0, code `no_fetch` | — |
| The API cannot be reached | The error `fetch` throws (a `TypeError`), not wrapped | `OpenWAFError`, status 0, code `unreachable` |
| Cancelled through `signal` | The `AbortError` `fetch` throws | — |

## Retries

The SDKs do not retry. Reads and simulations are safe to retry after a network error or a `5xx`. A write sent with `ifMatch` is safe too: if the first attempt was applied, the retry fails with `version_conflict` instead of applying the change twice.
