Errors
Directus uses conventional HTTP response codes to indicate the success or failure of an API request:
- Codes in the
2xxrange indicate success. - Codes in the
4xxrange indicate an error caused by the request (a missing parameter, a permission issue, a validation failure, etc.). - Codes in the
5xxrange 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:
{
"errors": [
{
"message": "You don't have permission to access this.",
"extensions": {
"code": "FORBIDDEN"
}
}
]
}
interface DirectusErrorResponse {
errors: DirectusError[];
}
interface DirectusError {
message: string;
extensions: {
code: string;
[key: string]: unknown;
};
}
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 and place the code under extensions.code on each error in the errors array.
HTTP Status Codes
| Status | Name | Description |
|---|---|---|
200 | OK | The request succeeded. |
204 | No Content | The request succeeded and there is no response body. |
400 | Bad Request | The request was invalid - usually a malformed payload, query, or validation failure. |
401 | Unauthorized | Authentication failed or no valid credentials were provided. |
403 | Forbidden | The authenticated user doesn't have permission to perform this action. |
404 | Not Found | The requested route or resource doesn't exist. |
405 | Method Not Allowed | The HTTP method isn't allowed on this endpoint. The Allow header lists supported methods. |
408 | Request Timeout | The operation took too long to complete. |
413 | Content Too Large | The uploaded payload exceeds the size limit. |
415 | Unsupported Media Type | The Content-Type of the request body isn't supported. |
416 | Range Not Satisfiable | The requested byte range can't be served for this file. |
422 | Unprocessable Content | The request was well-formed but couldn't be processed. |
429 | Too Many Requests | The rate limit has been exceeded. Back off and retry later. |
500 | Internal Server Error | An unexpected error occurred. Non-admin users see a generic message. |
503 | Service Unavailable | A required dependency or external service is unavailable. |
FORBIDDEN error rather than 404.Error Codes
The code value in extensions lets you handle errors programmatically without parsing the human-readable message. Built-in Directus error codes include:
| Error Code | Status | Description |
|---|---|---|
CONTAINS_NULL_VALUES | 400 | A field can't be set to non-nullable because existing rows contain null values. |
CONTENT_TOO_LARGE | 413 | Uploaded content exceeds the configured size limit. |
EMAIL_LIMIT_EXCEEDED | 429 | The email sending limit has been hit. |
FAILED_VALIDATION | 400 | A field value failed validation. |
FORBIDDEN | 403 | The user doesn't have permission to perform this action. |
GRAPHQL_EXECUTION | 400 | A GraphQL operation failed during execution setup. |
GRAPHQL_VALIDATION | 400 | A GraphQL operation failed validation. |
ILLEGAL_ASSET_TRANSFORMATION | 400 | The requested asset transformation parameters are not allowed. |
INTERNAL_SERVER_ERROR | 500 | An unexpected error occurred on the server. |
INVALID_CREDENTIALS | 401 | The provided email, password, or access token is wrong. |
INVALID_FOREIGN_KEY | 400 | A foreign key value doesn't reference an existing record. |
INVALID_INVITE | 400 | The invite link is no longer valid. |
INVALID_IP | 401 | The IP address isn't allow-listed for this user. |
INVALID_METADATA | 400 | Upload metadata is malformed. |
INVALID_OTP | 401 | The provided one-time password is incorrect. |
INVALID_PAYLOAD | 400 | The request body is invalid. |
INVALID_PATH_PARAMETER | 400 | A path parameter (like an ID) is malformed. |
INVALID_PROVIDER | 403 | The authentication provider is invalid or not enabled. |
INVALID_PROVIDER_CONFIG | 503 | The authentication provider is misconfigured. |
INVALID_QUERY | 400 | The query parameters can't be used as provided. |
INVALID_TOKEN | 403 | The access token is malformed or invalid. |
LIMIT_EXCEEDED | 403 | A configured limit (relations, depth, etc.) was exceeded. |
METHOD_NOT_ALLOWED | 405 | The HTTP method isn't allowed on this endpoint. |
NOT_NULL_VIOLATION | 400 | A required field was submitted as null. |
OUT_OF_DATE | 503 | The Directus instance is out of date for this operation. |
OUT_OF_TIME | 408 | The operation timed out. |
RANGE_NOT_SATISFIABLE | 416 | The byte range requested for a file can't be served. |
RECORD_NOT_UNIQUE | 400 | A unique constraint was violated. |
REQUESTS_EXCEEDED | 429 | The rate limit has been exceeded. |
ROUTE_NOT_FOUND | 404 | The requested endpoint doesn't exist. |
SERVICE_UNAVAILABLE | 503 | An external service Directus depends on is unavailable. |
TOKEN_EXPIRED | 401 | The access token is valid but has expired - refresh it. |
UNEXPECTED_RESPONSE | 503 | An external service returned an unexpected response. |
UNPROCESSABLE_CONTENT | 422 | The request was understood but can't be processed. |
UNSUPPORTED_MEDIA_TYPE | 415 | The Content-Type header or payload format isn't supported. |
USER_SUSPENDED | 401 | The user account is suspended. |
VALUE_OUT_OF_RANGE | 400 | A numeric value is outside the column's allowed range. |
VALUE_TOO_LONG | 400 | A value exceeds the column's maximum length. |
Handling Errors
REST API
Check the response status, then read errors[].extensions.code to branch on specific failure modes:
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:
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:
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:
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:
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:
catch (err) {
for (const error of err.errors ?? []) {
const { code, field, collection } = error.extensions ?? {};
if (field) {
showFieldError(field, error.message);
}
}
}
Next Steps
- Review authentication for token and session handling.
- Read the SDK guide for the full client API.
Get once-a-month release notes & real‑world code tips...no fluff. 🐰