> Source: https://wexa.ai/docs/tools/errors

# Errors

This page is the whole set, and the place to look when you want to handle errors once rather than per
call. A tool page lists only what is specific to that tool, and links back here for the rest.

## The common set

Seven classes are reachable from every tool, because every call runs the same
lifecycle: `AuthError`, `ForbiddenError`, `ValidationError`, `PolicyDenied`, `QuotaExceeded`,
`UpstreamError` and `TimeoutError`. Handle those once. What varies from tool to tool is only:

| Class | Reachable from |
| --- | --- |
| `NotFound` | tools that take an id or a name that must already resolve to an object |
| `ApprovalRequired` | the 32 tools marked consequential, which a policy may hold for a person; the project's write gate (on by default) also holds the 31 that write (all but `query-context`) when an API key or SDK session calls them |
| `ConfigurationError` | the 56 plane-gated tools; the six always-registered tools cannot reach it |

Each tool page's error table is that tool's slice of the three.

## How an error is chosen

The gateway answers a refused call with an error string and an HTTP status. Both SDKs turn that pair
into a typed exception, choosing on the error string first and the status second, so the same refusal
becomes the same class in both languages.

On REST and on the Wexa MCP server there is no typed error: REST gives you the status and the
`error` / `error_description` pair directly, and the MCP server reports a refusal as a JSON-RPC error
or as `isError: true` with the message in `content[0].text`.

## The classes

Both SDKs share one base class, `WexaError`. Catching it catches everything below.

| Class | Status | Raised when |
| --- | --- | --- |
| `AuthError` | 401 | The credential is missing, malformed or revoked. |
| `ForbiddenError` | 403 | The credential lacks the grant this tool needs, or its scope does not cover the project. |
| `PolicyDenied` | 403 | A policy rule refused this specific call. Subclasses `ForbiddenError`, so catching that catches this too. The refusal names the rule. |
| `ValidationError` | 400 / 422 | An argument is missing, of the wrong type, or is one the platform binds itself. Also raised client-side, before any request, for a server-bound argument. |
| `ApprovalRequired` | 202 | A person must approve this call before it runs. Carries the approval id. |
| `ApprovalError` | — | The approval was rejected, or the wait for it failed. |
| `QuotaExceeded` | 429 | The project or organization is over its request rate or its credit balance. |
| `NotFound` | 404 | An id in the arguments matches nothing in this project. |
| `ConflictError` | 409 | The change collides with the object's current state. |
| `UpstreamError` | 5xx | A service behind the gateway failed or was unreachable. |
| `ConfigurationError` | 5xx | The tool is not available to you. Retrying will not help. |

## Where the two SDKs differ

The classes are not quite identical, and code that catches by name has to account for it.

| | TypeScript | Python |
| --- | --- | --- |
| Timeout | `TimeoutError` | **`TimeoutError_`** — the trailing underscore avoids shadowing Python's builtin, and it subclasses `UpstreamError` rather than standing alone |
| Transport failure | `TransportError` | no equivalent; a transport failure surfaces as `UpstreamError` |
| Total classes | 13 plus `WexaError` | 12 plus `WexaError` |

## Reading an error

Both SDKs attach the governance ids to the exception, and they are the fastest way to find out why a
call was refused.

```
ValidationError: malformed after_id "<after_id>": expected a table_id from a previous page
[lifecycle_id=qlc_000050 trace_id=a43def45c69440ac982ecd49b4396df1]
```

The `lifecycle_id` is the handle to the audit record for the call. Quote it when asking why something
was allowed, refused or held.

## See also

[The tool index](/docs/tools/overview) for the tools themselves.
