# Errors

> Learn how Directus returns errors over REST, GraphQL, and the SDK. Includes core error codes, HTTP status codes, response shape, and patterns for handling errors in your application.

Directus uses conventional HTTP response codes to indicate the success or failure of an API request:

- Codes in the `2xx` range indicate success.
- Codes in the `4xx` range indicate an error caused by the request (a missing parameter, a permission issue, a validation failure, etc.).
- Codes in the `5xx` range indicate an error on the server.

All errors are returned in a consistent JSON shape so you can handle them programmatically using the `code` value in `extensions`.

## Error Response Shape

Every error response from the REST API follows the same structure:

<code-group>

```json [JSON]
{
    "errors": [
        {
            "message": "You don't have permission to access this.",
            "extensions": {
                "code": "FORBIDDEN"
            }
        }
    ]
}
```

```ts [TypeScript]
interface DirectusErrorResponse {
    errors: DirectusError[];
}

interface DirectusError {
    message: string;
    extensions: {
        code: string;
        [key: string]: unknown;
    };
}
```

</code-group>

A single response can contain multiple errors. Some errors include additional fields in `extensions` with context about what went wrong (the offending `collection`, `field`, or `value`, for example). In development mode, a `stack` trace is included in `extensions` to help with debugging.

GraphQL responses follow the [GraphQL spec](https://spec.graphql.org/October2021/#sec-Errors) and place the `code` under `extensions.code` on each error in the `errors` array.

## HTTP Status Codes

<table>
<thead>
  <tr>
    <th>
      Status
    </th>
    
    <th>
      Name
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        200
      </code>
    </td>
    
    <td>
      OK
    </td>
    
    <td>
      The request succeeded.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        204
      </code>
    </td>
    
    <td>
      No Content
    </td>
    
    <td>
      The request succeeded and there is no response body.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        400
      </code>
    </td>
    
    <td>
      Bad Request
    </td>
    
    <td>
      The request was invalid - usually a malformed payload, query, or validation failure.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        401
      </code>
    </td>
    
    <td>
      Unauthorized
    </td>
    
    <td>
      Authentication failed or no valid credentials were provided.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        403
      </code>
    </td>
    
    <td>
      Forbidden
    </td>
    
    <td>
      The authenticated user doesn't have permission to perform this action.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        404
      </code>
    </td>
    
    <td>
      Not Found
    </td>
    
    <td>
      The requested route or resource doesn't exist.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        405
      </code>
    </td>
    
    <td>
      Method Not Allowed
    </td>
    
    <td>
      The HTTP method isn't allowed on this endpoint. The <code>
        Allow
      </code>
      
       header lists supported methods.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        408
      </code>
    </td>
    
    <td>
      Request Timeout
    </td>
    
    <td>
      The operation took too long to complete.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        413
      </code>
    </td>
    
    <td>
      Content Too Large
    </td>
    
    <td>
      The uploaded payload exceeds the size limit.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        415
      </code>
    </td>
    
    <td>
      Unsupported Media Type
    </td>
    
    <td>
      The <code>
        Content-Type
      </code>
      
       of the request body isn't supported.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        416
      </code>
    </td>
    
    <td>
      Range Not Satisfiable
    </td>
    
    <td>
      The requested byte range can't be served for this file.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        422
      </code>
    </td>
    
    <td>
      Unprocessable Content
    </td>
    
    <td>
      The request was well-formed but couldn't be processed.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        429
      </code>
    </td>
    
    <td>
      Too Many Requests
    </td>
    
    <td>
      The rate limit has been exceeded. Back off and retry later.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        500
      </code>
    </td>
    
    <td>
      Internal Server Error
    </td>
    
    <td>
      An unexpected error occurred. Non-admin users see a generic message.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        503
      </code>
    </td>
    
    <td>
      Service Unavailable
    </td>
    
    <td>
      A required dependency or external service is unavailable.
    </td>
  </tr>
</tbody>
</table>

<callout icon="i-lucide-shield">

To prevent revealing which items exist, all actions for non-existing items return a `FORBIDDEN` error rather than `404`.

</callout>

## Error Codes

The `code` value in `extensions` lets you handle errors programmatically without parsing the human-readable `message`. Built-in Directus error codes include:

<table>
<thead>
  <tr>
    <th>
      Error Code
    </th>
    
    <th>
      Status
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        CONTAINS_NULL_VALUES
      </code>
    </td>
    
    <td>
      400
    </td>
    
    <td>
      A field can't be set to non-nullable because existing rows contain null values.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        CONTENT_TOO_LARGE
      </code>
    </td>
    
    <td>
      413
    </td>
    
    <td>
      Uploaded content exceeds the configured size limit.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        EMAIL_LIMIT_EXCEEDED
      </code>
    </td>
    
    <td>
      429
    </td>
    
    <td>
      The email sending limit has been hit.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        FAILED_VALIDATION
      </code>
    </td>
    
    <td>
      400
    </td>
    
    <td>
      A field value failed validation.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        FORBIDDEN
      </code>
    </td>
    
    <td>
      403
    </td>
    
    <td>
      The user doesn't have permission to perform this action.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        GRAPHQL_EXECUTION
      </code>
    </td>
    
    <td>
      400
    </td>
    
    <td>
      A GraphQL operation failed during execution setup.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        GRAPHQL_VALIDATION
      </code>
    </td>
    
    <td>
      400
    </td>
    
    <td>
      A GraphQL operation failed validation.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ILLEGAL_ASSET_TRANSFORMATION
      </code>
    </td>
    
    <td>
      400
    </td>
    
    <td>
      The requested asset transformation parameters are not allowed.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        INTERNAL_SERVER_ERROR
      </code>
    </td>
    
    <td>
      500
    </td>
    
    <td>
      An unexpected error occurred on the server.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        INVALID_CREDENTIALS
      </code>
    </td>
    
    <td>
      401
    </td>
    
    <td>
      The provided email, password, or access token is wrong.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        INVALID_FOREIGN_KEY
      </code>
    </td>
    
    <td>
      400
    </td>
    
    <td>
      A foreign key value doesn't reference an existing record.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        INVALID_INVITE
      </code>
    </td>
    
    <td>
      400
    </td>
    
    <td>
      The invite link is no longer valid.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        INVALID_IP
      </code>
    </td>
    
    <td>
      401
    </td>
    
    <td>
      The IP address isn't allow-listed for this user.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        INVALID_METADATA
      </code>
    </td>
    
    <td>
      400
    </td>
    
    <td>
      Upload metadata is malformed.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        INVALID_OTP
      </code>
    </td>
    
    <td>
      401
    </td>
    
    <td>
      The provided one-time password is incorrect.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        INVALID_PAYLOAD
      </code>
    </td>
    
    <td>
      400
    </td>
    
    <td>
      The request body is invalid.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        INVALID_PATH_PARAMETER
      </code>
    </td>
    
    <td>
      400
    </td>
    
    <td>
      A path parameter (like an ID) is malformed.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        INVALID_PROVIDER
      </code>
    </td>
    
    <td>
      403
    </td>
    
    <td>
      The authentication provider is invalid or not enabled.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        INVALID_PROVIDER_CONFIG
      </code>
    </td>
    
    <td>
      503
    </td>
    
    <td>
      The authentication provider is misconfigured.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        INVALID_QUERY
      </code>
    </td>
    
    <td>
      400
    </td>
    
    <td>
      The query parameters can't be used as provided.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        INVALID_TOKEN
      </code>
    </td>
    
    <td>
      403
    </td>
    
    <td>
      The access token is malformed or invalid.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        LIMIT_EXCEEDED
      </code>
    </td>
    
    <td>
      403
    </td>
    
    <td>
      A configured limit (relations, depth, etc.) was exceeded.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        METHOD_NOT_ALLOWED
      </code>
    </td>
    
    <td>
      405
    </td>
    
    <td>
      The HTTP method isn't allowed on this endpoint.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        NOT_NULL_VIOLATION
      </code>
    </td>
    
    <td>
      400
    </td>
    
    <td>
      A required field was submitted as null.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        OUT_OF_DATE
      </code>
    </td>
    
    <td>
      503
    </td>
    
    <td>
      The Directus instance is out of date for this operation.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        OUT_OF_TIME
      </code>
    </td>
    
    <td>
      408
    </td>
    
    <td>
      The operation timed out.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        RANGE_NOT_SATISFIABLE
      </code>
    </td>
    
    <td>
      416
    </td>
    
    <td>
      The byte range requested for a file can't be served.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        RECORD_NOT_UNIQUE
      </code>
    </td>
    
    <td>
      400
    </td>
    
    <td>
      A unique constraint was violated.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        REQUESTS_EXCEEDED
      </code>
    </td>
    
    <td>
      429
    </td>
    
    <td>
      The rate limit has been exceeded.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ROUTE_NOT_FOUND
      </code>
    </td>
    
    <td>
      404
    </td>
    
    <td>
      The requested endpoint doesn't exist.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        SERVICE_UNAVAILABLE
      </code>
    </td>
    
    <td>
      503
    </td>
    
    <td>
      An external service Directus depends on is unavailable.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        TOKEN_EXPIRED
      </code>
    </td>
    
    <td>
      401
    </td>
    
    <td>
      The access token is valid but has expired - refresh it.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        UNEXPECTED_RESPONSE
      </code>
    </td>
    
    <td>
      503
    </td>
    
    <td>
      An external service returned an unexpected response.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        UNPROCESSABLE_CONTENT
      </code>
    </td>
    
    <td>
      422
    </td>
    
    <td>
      The request was understood but can't be processed.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        UNSUPPORTED_MEDIA_TYPE
      </code>
    </td>
    
    <td>
      415
    </td>
    
    <td>
      The <code>
        Content-Type
      </code>
      
       header or payload format isn't supported.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        USER_SUSPENDED
      </code>
    </td>
    
    <td>
      401
    </td>
    
    <td>
      The user account is suspended.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        VALUE_OUT_OF_RANGE
      </code>
    </td>
    
    <td>
      400
    </td>
    
    <td>
      A numeric value is outside the column's allowed range.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        VALUE_TOO_LONG
      </code>
    </td>
    
    <td>
      400
    </td>
    
    <td>
      A value exceeds the column's maximum length.
    </td>
  </tr>
</tbody>
</table>

<callout icon="i-lucide-info">

Extensions, flows, imports, and upload handlers can return additional error codes. Handle unknown codes with a generic fallback.

</callout>

## Handling Errors

### REST API

Check the response status, then read `errors[].extensions.code` to branch on specific failure modes:

```js
const response = await fetch('https://example.directus.app/items/articles', {
    headers: { Authorization: `Bearer ${token}` },
});

const body = await response.json();

if (!response.ok) {
    const error = body.errors?.[0];
    const code = error?.extensions?.code;

    switch (code) {
        case 'TOKEN_EXPIRED':
            // Refresh the token and retry
            break;
        case 'FORBIDDEN':
            // Show a permissions message to the user
            break;
        case 'REQUESTS_EXCEEDED':
            // Back off and retry later
            break;
        default:
            console.error(error?.message);
    }
}
```

### SDK

The SDK throws the parsed error response when a request fails. Wrap calls in `try/catch` and inspect `errors[].extensions.code`:

```ts
import { createDirectus, rest, readItems } from '@directus/sdk';

const directus = createDirectus('https://example.directus.app').with(rest());

try {
    const articles = await directus.request(readItems('articles'));
} catch (err) {
    const error = err.errors?.[0];
    const code = error?.extensions?.code;

    if (code === 'TOKEN_EXPIRED') {
        // Refresh the token and retry
    } else if (code === 'FORBIDDEN') {
        // Handle permission denial
    } else {
        console.error(error?.message ?? err);
    }
}
```

The SDK error includes the raw `response`, so you can read `err.response.status` when you use the default fetch client. Prefer `code` for programmatic handling because it is stable across transports.

### GraphQL

GraphQL resolver errors can return `200 OK` with an `errors` array in the response body. Request-level failures, such as invalid GraphQL syntax or validation errors, can return a non-2xx HTTP status. Check both the response status and the `errors` array:

```js
const response = await fetch('https://example.directus.app/graphql', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ query: '{ articles { id title } }' }),
});

const body = await response.json();
const { data, errors } = body;

if (!response.ok || errors) {
    for (const error of errors ?? []) {
        const code = error.extensions?.code;

        if (code === 'FORBIDDEN') {
            // Handle permission denial
        }
    }
}
```

## Common Patterns

### Refreshing an Expired Token

`TOKEN_EXPIRED` indicates the request was authenticated but the access token has expired. Use the refresh token to get a new pair and retry the request:

```ts
import { createDirectus, rest, authentication } from '@directus/sdk';

const directus = createDirectus('https://example.directus.app')
    .with(authentication())
    .with(rest());

try {
    await directus.request(/* ... */);
} catch (err) {
    if (err.errors?.[0]?.extensions?.code === 'TOKEN_EXPIRED') {
        await directus.refresh();
        await directus.request(/* ... */);
    }
}
```

### Backing Off on Rate Limits

When you receive `REQUESTS_EXCEEDED`, retry with exponential backoff rather than retrying immediately:

```ts
async function withRetry(fn, attempts = 3) {
    for (let i = 0; i < attempts; i++) {
        try {
            return await fn();
        } catch (err) {
            const code = err.errors?.[0]?.extensions?.code;
            if (code !== 'REQUESTS_EXCEEDED' || i === attempts - 1) throw err;
            await new Promise((r) => setTimeout(r, 2 ** i * 1000));
        }
    }
}
```

### Surfacing Validation Errors

`FAILED_VALIDATION` errors include the offending `field`, `path`, and validation `type` in `extensions`. Database constraint errors like `RECORD_NOT_UNIQUE`, `NOT_NULL_VIOLATION`, `INVALID_FOREIGN_KEY`, `VALUE_OUT_OF_RANGE`, and `VALUE_TOO_LONG` can include `collection`, `field`, or `value`. `INVALID_PAYLOAD` includes a `reason`. Use these fields to display actionable errors in your UI:

```ts
catch (err) {
    for (const error of err.errors ?? []) {
        const { code, field, collection } = error.extensions ?? {};
        if (field) {
            showFieldError(field, error.message);
        }
    }
}
```

## Next Steps

- Review [authentication](/guides/auth/tokens-cookies) for token and session handling.
- Read the [SDK guide](/guides/connect/sdk) for the full client API.
