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"
}
}
| Field | Required | Description |
|---|---|---|
message | Yes | Human-readable error description |
locations | No | Where in the query the error occurred |
path | No | Which field in the response failed |
extensions | No | Custom 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
| Concept | Description |
|---|---|
| Partial Response | Data and errors can coexist |
| Null Propagation | Failed non-null fields null their parent |
| Error Path | Shows which field failed |
| Extensions | Machine-readable error metadata |
| Error Codes | Standardized codes for client handling |
What's Next?
In the next chapter, we'll explore Validation & Execution - how GraphQL processes queries from parsing to response.