Skip to main content

Error Handling

Understand how GraphQL handles errors and how to design for graceful failures.


GraphQL Error Philosophy

GraphQL handles errors differently from REST. The key insight:

A GraphQL response can contain both data AND errors.

This enables partial responses - some fields succeed while others fail, and the client receives both.

{
"data": {
"movie": {
"title": "Inception",
"director": null
}
},
"errors": [
{
"message": "Director service unavailable",
"path": ["movie", "director"]
}
]
}

The client gets the movie title even though the director lookup failed.


Response Structure

Every GraphQL response has this structure:

{
"data": { ... }, // Query results (may be partial)
"errors": [ ... ], // Array of errors (if any)
"extensions": { ... } // Optional metadata
}

Success (No Errors)

{
"data": {
"movie": {
"title": "The Matrix",
"releaseYear": 1999
}
}
}

Partial Success

{
"data": {
"movie": {
"title": "The Matrix",
"externalRating": null
}
},
"errors": [
{
"message": "External rating service timeout",
"path": ["movie", "externalRating"]
}
]
}

Complete Failure

{
"data": null,
"errors": [
{
"message": "Authentication required",
"extensions": {
"code": "UNAUTHENTICATED"
}
}
]
}

Error Object Anatomy

Each error in the errors array has these fields:

{
"message": "Cannot return null for non-nullable field Movie.title",
"locations": [
{ "line": 3, "column": 5 }
],
"path": ["movie", "title"],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"timestamp": "2024-01-15T10:30:00Z"
}
}
FieldRequiredDescription
messageYesHuman-readable error description
locationsNoWhere in the query the error occurred
pathNoWhich field in the response failed
extensionsNoCustom metadata (error codes, etc.)

Error Types

1. Syntax Errors

Invalid GraphQL syntax - the query can't be parsed.

query {
movie(id: "1" { # Missing closing parenthesis
title
}
}
{
"errors": [
{
"message": "Syntax Error: Expected Name, found {",
"locations": [{ "line": 2, "column": 18 }]
}
]
}

Note: No data field - the query couldn't be executed at all.

2. Validation Errors

Query is syntactically valid but violates the schema.

query {
movie(id: "1") {
title
nonExistentField # Field doesn't exist
}
}
{
"errors": [
{
"message": "Cannot query field 'nonExistentField' on type 'Movie'",
"locations": [{ "line": 4, "column": 5 }]
}
]
}

3. Execution Errors

Query is valid but something went wrong during execution.

query {
movie(id: "1") {
title
director {
name
}
}
}
{
"data": {
"movie": {
"title": "The Matrix",
"director": null
}
},
"errors": [
{
"message": "Director service unavailable",
"path": ["movie", "director"],
"extensions": {
"code": "SERVICE_UNAVAILABLE"
}
}
]
}

Null Propagation

When a field fails, GraphQL follows null propagation rules:

┌─────────────────────────────────────────────────────────────────────┐
│ NULL PROPAGATION │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Schema: director: Director (nullable) │
│ Error in director → director: null │
│ Result: Query continues, other fields returned │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ Schema: director: Director! (non-null) │
│ Error in director → null propagates UP to parent │
│ │
│ If parent is nullable: │
│ movie: null │
│ │
│ If parent is also non-null: │
│ Propagates further up until nullable field or data: null │
│ │
└─────────────────────────────────────────────────────────────────────┘

Example: Nullable Field

type Movie {
title: String!
director: Director # Nullable
}

If director fails:

{
"data": {
"movie": {
"title": "The Matrix",
"director": null
}
},
"errors": [{ "message": "...", "path": ["movie", "director"] }]
}

Example: Non-Null Field

type Movie {
title: String!
director: Director! # Non-null
}

If director fails:

{
"data": {
"movie": null
},
"errors": [{ "message": "...", "path": ["movie", "director"] }]
}

The entire movie becomes null because director was required but couldn't be returned.


Error Extensions

Use extensions for machine-readable error information:

{
"errors": [
{
"message": "You don't have permission to view this movie",
"path": ["movie"],
"extensions": {
"code": "FORBIDDEN",
"requiredPermission": "movies:read",
"userPermissions": ["movies:list"]
}
}
]
}

Common Error Codes

# Convention (not standardized, but widely used)
UNAUTHENTICATED # No valid credentials
FORBIDDEN # Authenticated but not authorized
BAD_USER_INPUT # Invalid argument values
NOT_FOUND # Resource doesn't exist
INTERNAL_ERROR # Unexpected server error
SERVICE_UNAVAILABLE # External service down
RATE_LIMITED # Too many requests

Designing for Errors

Strategy 1: Nullable Fields for Graceful Degradation

type Movie {
id: ID!
title: String!
# These can fail independently
director: Director # Nullable - can fail gracefully
externalRating: Float # Nullable - external service might be down
streamingLinks: [Link!] # Nullable - might not be available
}

Strategy 2: Result Types for Expected Errors

For mutations where errors are expected (validation, business rules):

type Mutation {
createMovie(input: CreateMovieInput!): CreateMovieResult!
}

union CreateMovieResult = CreateMovieSuccess | CreateMovieError

type CreateMovieSuccess {
movie: Movie!
}

type CreateMovieError {
message: String!
code: ErrorCode!
field: String
}

Query:

mutation {
createMovie(input: $input) {
... on CreateMovieSuccess {
movie { id title }
}
... on CreateMovieError {
message
code
field
}
}
}

Strategy 3: Errors as Data

For multiple field-level errors:

type CreateMoviePayload {
movie: Movie
errors: [UserError!]!
}

type UserError {
field: String!
message: String!
code: String!
}

Response:

{
"data": {
"createMovie": {
"movie": null,
"errors": [
{ "field": "releaseYear", "message": "Must be after 1888", "code": "INVALID_YEAR" },
{ "field": "title", "message": "Already exists", "code": "DUPLICATE" }
]
}
}
}

Client-Side Error Handling

Check for Errors First

const response = await graphqlClient.query(GET_MOVIE, { id: "1" });

if (response.errors) {
// Handle errors
response.errors.forEach(error => {
console.error(`Error at ${error.path}: ${error.message}`);

// Check error code
if (error.extensions?.code === 'UNAUTHENTICATED') {
redirectToLogin();
}
});
}

// Even with errors, data might be partially available
if (response.data?.movie) {
displayMovie(response.data.movie);
}

Categorize by Error Code

function handleGraphQLErrors(errors) {
for (const error of errors) {
switch (error.extensions?.code) {
case 'UNAUTHENTICATED':
return { action: 'redirect', target: '/login' };
case 'FORBIDDEN':
return { action: 'show', message: 'Access denied' };
case 'NOT_FOUND':
return { action: 'show', message: 'Not found' };
case 'BAD_USER_INPUT':
return { action: 'validate', fields: error.extensions.fields };
default:
return { action: 'show', message: 'Something went wrong' };
}
}
}

Best Practices

1. Use Meaningful Error Messages

// ❌ Bad
{ "message": "Error" }

// ✅ Good
{ "message": "Movie with ID '999' not found" }

2. Include Error Codes

// ❌ Bad - client must parse message
{ "message": "You must be logged in" }

// ✅ Good - machine-readable code
{
"message": "You must be logged in",
"extensions": { "code": "UNAUTHENTICATED" }
}

3. Don't Expose Internal Details

// ❌ Bad - exposes implementation
{ "message": "SQLException: connection refused to postgres:5432" }

// ✅ Good - user-friendly, logs internal detail
{ "message": "Database temporarily unavailable" }

4. Use Path for Field-Level Errors

{
"message": "Rating must be between 0 and 10",
"path": ["createReview", "input", "rating"],
"extensions": { "code": "BAD_USER_INPUT" }
}

Summary

ConceptDescription
Partial ResponseData and errors can coexist
Null PropagationFailed non-null fields null their parent
Error PathShows which field failed
ExtensionsMachine-readable error metadata
Error CodesStandardized codes for client handling

What's Next?

In the next chapter, we'll explore Validation & Execution - how GraphQL processes queries from parsing to response.