Skip to content

Error Handling

All Grant API errors follow a consistent format across both REST and GraphQL, with machine-readable codes and automatic localization.

Response Format

REST:

json
{
  "error": "Localized error message",
  "code": "ERROR_CODE",
  "extensions": {
    "field": "additionalContext"
  }
}

GraphQL:

json
{
  "data": null,
  "errors": [
    {
      "message": "Localized error message",
      "extensions": { "code": "ERROR_CODE" }
    }
  ]
}

The code field is stable and safe for programmatic handling. The error/message field is human-readable and localized.

HTTP Status Codes

StatusError ClassDescription
400BadRequestErrorMalformed request or invalid JSON
400ValidationErrorInput validation failed (field-level details in extensions)
401AuthenticationErrorMissing, invalid, or expired token
403AuthorizationErrorToken valid but insufficient permissions
404NotFoundErrorResource does not exist or is not accessible
409ConflictErrorDuplicate resource (e.g., email already exists)
429Rate limit exceeded (Retry-After header included)
500ApiErrorInternal server error

Error Codes

CodeStatusWhen
BAD_USER_INPUT400Invalid JSON or malformed request body
VALIDATION_ERROR400Zod schema validation failed
UNAUTHENTICATED401No token, expired token, revoked session
FORBIDDEN403User lacks the required permission
NOT_FOUND404Entity not found in the requested scope
CONFLICT409Unique constraint violation
RATE_LIMIT_EXCEEDED429Too many requests — check Retry-After header
INTERNAL_ERROR500Unexpected server error

Error Examples

Authentication (401)

http
GET /api/users
Authorization: Bearer <expired_token>
json
{ "error": "Invalid or expired token", "code": "UNAUTHENTICATED" }

Authorization (403)

http
DELETE /api/organizations/<id>
Authorization: Bearer <valid_token>
json
{ "error": "You are not authorized to perform this action", "code": "FORBIDDEN" }

Not Found (404)

http
GET /api/users/<nonexistent_id>
json
{ "error": "User not found", "code": "NOT_FOUND" }

Validation (400)

http
POST /api/organizations
{ "name": "" }
json
{
  "error": "Organization name is required",
  "code": "VALIDATION_ERROR",
  "extensions": { "field": "name" }
}

Conflict (409)

http
POST /api/users
{ "email": "existing@example.com", ... }
json
{
  "error": "A User with this email already exists",
  "code": "CONFLICT",
  "extensions": { "resource": "User", "field": "email" }
}

Rate Limit (429)

http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
json
{ "error": "Too many requests", "code": "RATE_LIMIT_EXCEEDED" }

Internal Error (500)

json
{ "error": "Internal server error", "code": "INTERNAL_ERROR" }

In development mode, the response includes a stack field with the full stack trace.

Localization

Error messages are automatically localized based on the Accept-Language header:

bash
# English (default)
curl -H "Accept-Language: en" http://localhost:4000/api/users/invalid-id
# → { "error": "User not found", "code": "NOT_FOUND" }

# German
curl -H "Accept-Language: de" http://localhost:4000/api/users/invalid-id
# → { "error": "Benutzer nicht gefunden", "code": "NOT_FOUND" }

Supported languages: English (en), German (de). The code field is always the same regardless of language — use it for programmatic handling, and display the error message to users.

See Internationalization for adding languages.


Related:

Released under the MIT License.