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:

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

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

StatusNameDescription
200OKThe request succeeded.
204No ContentThe request succeeded and there is no response body.
400Bad RequestThe request was invalid - usually a malformed payload, query, or validation failure.
401UnauthorizedAuthentication failed or no valid credentials were provided.
403ForbiddenThe authenticated user doesn't have permission to perform this action.
404Not FoundThe requested route or resource doesn't exist.
405Method Not AllowedThe HTTP method isn't allowed on this endpoint. The Allow header lists supported methods.
408Request TimeoutThe operation took too long to complete.
413Content Too LargeThe uploaded payload exceeds the size limit.
415Unsupported Media TypeThe Content-Type of the request body isn't supported.
416Range Not SatisfiableThe requested byte range can't be served for this file.
422Unprocessable ContentThe request was well-formed but couldn't be processed.
429Too Many RequestsThe rate limit has been exceeded. Back off and retry later.
500Internal Server ErrorAn unexpected error occurred. Non-admin users see a generic message.
503Service UnavailableA required dependency or external service is unavailable.
To prevent revealing which items exist, all actions for non-existing items return a 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 CodeStatusDescription
CONTAINS_NULL_VALUES400A field can't be set to non-nullable because existing rows contain null values.
CONTENT_TOO_LARGE413Uploaded content exceeds the configured size limit.
EMAIL_LIMIT_EXCEEDED429The email sending limit has been hit.
FAILED_VALIDATION400A field value failed validation.
FORBIDDEN403The user doesn't have permission to perform this action.
GRAPHQL_EXECUTION400A GraphQL operation failed during execution setup.
GRAPHQL_VALIDATION400A GraphQL operation failed validation.
ILLEGAL_ASSET_TRANSFORMATION400The requested asset transformation parameters are not allowed.
INTERNAL_SERVER_ERROR500An unexpected error occurred on the server.
INVALID_CREDENTIALS401The provided email, password, or access token is wrong.
INVALID_FOREIGN_KEY400A foreign key value doesn't reference an existing record.
INVALID_INVITE400The invite link is no longer valid.
INVALID_IP401The IP address isn't allow-listed for this user.
INVALID_METADATA400Upload metadata is malformed.
INVALID_OTP401The provided one-time password is incorrect.
INVALID_PAYLOAD400The request body is invalid.
INVALID_PATH_PARAMETER400A path parameter (like an ID) is malformed.
INVALID_PROVIDER403The authentication provider is invalid or not enabled.
INVALID_PROVIDER_CONFIG503The authentication provider is misconfigured.
INVALID_QUERY400The query parameters can't be used as provided.
INVALID_TOKEN403The access token is malformed or invalid.
LIMIT_EXCEEDED403A configured limit (relations, depth, etc.) was exceeded.
METHOD_NOT_ALLOWED405The HTTP method isn't allowed on this endpoint.
NOT_NULL_VIOLATION400A required field was submitted as null.
OUT_OF_DATE503The Directus instance is out of date for this operation.
OUT_OF_TIME408The operation timed out.
RANGE_NOT_SATISFIABLE416The byte range requested for a file can't be served.
RECORD_NOT_UNIQUE400A unique constraint was violated.
REQUESTS_EXCEEDED429The rate limit has been exceeded.
ROUTE_NOT_FOUND404The requested endpoint doesn't exist.
SERVICE_UNAVAILABLE503An external service Directus depends on is unavailable.
TOKEN_EXPIRED401The access token is valid but has expired - refresh it.
UNEXPECTED_RESPONSE503An external service returned an unexpected response.
UNPROCESSABLE_CONTENT422The request was understood but can't be processed.
UNSUPPORTED_MEDIA_TYPE415The Content-Type header or payload format isn't supported.
USER_SUSPENDED401The user account is suspended.
VALUE_OUT_OF_RANGE400A numeric value is outside the column's allowed range.
VALUE_TOO_LONG400A value exceeds the column's maximum length.
Extensions, flows, imports, and upload handlers can return additional error codes. Handle unknown codes with a generic fallback.

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

Get once-a-month release notes & real‑world code tips...no fluff. 🐰