GraphQL Error Handling Best Practices
GraphQL provides flexible error handling mechanisms, but implementing error handling correctly is crucial for building robust APIs. Here are key strategies and best practices for GraphQL error handling.
1. GraphQL Error Structure
{
"data": {
"user": null
},
"errors": [
{
"message": "User not found",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": ["user"],
"extensions": {
"code": "NOT_FOUND",
"timestamp": "2024-01-01T12:00:00Z"
}
}
]
}
Error Field Descriptions
- message: Error description
- locations: Error location in the query
- path: Field path where error occurred
- extensions: Custom extension information
2. Custom Error Classes
Creating Error Class Hierarchy
class GraphQLError extends Error {
constructor(message, code, extensions = {}) {
super(message);
this.name = 'GraphQLError';
this.code = code;
this.extensions = extensions;
}
}
class ValidationError extends GraphQLError {
constructor(message, field) {
super(message, 'VALIDATION_ERROR', { field });
}
}
class NotFoundError extends GraphQLError {
constructor(resource, id) {
super(`${resource} with id ${id} not found`, 'NOT_FOUND', { resource, id });
}
}
class AuthenticationError extends GraphQLError {
constructor(message = 'Authentication required') {
super(message, 'AUTHENTICATION_ERROR');
}
}
class AuthorizationError extends GraphQLError {
constructor(message = 'Not authorized') {
super(message, 'AUTHORIZATION_ERROR');
}
}
class ConflictError extends GraphQLError {
constructor(message) {
super(message, 'CONFLICT_ERROR');
}
}
class RateLimitError extends GraphQLError {
constructor(retryAfter) {
super('Rate limit exceeded', 'RATE_LIMIT_ERROR', { retryAfter });
}
}
3. Throwing Errors in Resolvers
Basic Error Throwing
const resolvers = {
Query: {
user: async (_, { id }) => {
const user = await User.findById(id);
if (!user) {
throw new NotFoundError('User', id);
}
return user;
}
},
Mutation: {
createUser: async (_, { input }) => {
// Validate input
if (!input.email || !isValidEmail(input.email)) {
throw new ValidationError('Invalid email address', 'email');
}
// Check if user already exists
const existingUser = await User.findByEmail(input.email);
if (existingUser) {
throw new ConflictError('User with this email already exists');
}
return User.create(input);
}
}
};
Partial Error Handling
const resolvers = {
Mutation: {
createUsers: async (_, { inputs }) => {
const results = [];
const errors = [];
for (const input of inputs) {
try {
const user = await User.create(input);
results.push({ success: true, user });
} catch (error) {
errors.push({
success: false,
input,
error: error.message
});
}
}
return {
results,
errors,
total: inputs.length,
successCount: results.length,
failureCount: errors.length
};
}
}
};
const formatError = (error) => {
// Handle custom errors
if (error.originalError instanceof GraphQLError) {
return {
message: error.message,
code: error.originalError.code,
extensions: error.originalError.extensions
};
}
// Handle validation errors
if (error.originalError instanceof ValidationError) {
return {
message: error.message,
code: 'VALIDATION_ERROR',
field: error.originalError.field
};
}
// Don't expose detailed errors in production
if (process.env.NODE_ENV === 'production') {
return {
message: 'Internal server error',
code: 'INTERNAL_SERVER_ERROR'
};
}
// Return full error information in development
return {
message: error.message,
code: 'INTERNAL_SERVER_ERROR',
stack: error.stack
};
};
const server = new ApolloServer({
typeDefs,
resolvers,
formatError
});
5. Error Logging
Structured Error Logging
const winston = require('winston');
const logger = winston.createLogger({
level: 'error',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log' })
]
});
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
{
requestDidStart: () => ({
didEncounterErrors: (context) => {
context.errors.forEach((error) => {
logger.error('GraphQL Error', {
message: error.message,
code: error.extensions?.code,
path: error.path,
locations: error.locations,
query: context.request.query,
variables: context.request.variables
});
});
}
})
}
]
});
6. Error Recovery Strategies
Fallback Handling
const resolvers = {
Query: {
userProfile: async (_, { id }, { dataSources }) => {
try {
// Try to get from primary data source
return await dataSources.userAPI.getUser(id);
} catch (error) {
// If primary source fails, use cached data
const cachedUser = await redis.get(`user:${id}`);
if (cachedUser) {
logger.warn('Using cached user data due to API failure', { userId: id });
return JSON.parse(cachedUser);
}
// If cache also fails, return default data
logger.error('Failed to fetch user data', { userId: id, error });
return {
id,
name: 'Unknown User',
isFallback: true
};
}
}
}
};
Retry Mechanism
async function retryOperation(operation, maxRetries = 3, delay = 1000) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
lastError = error;
// If it's a retryable error, wait and retry
if (isRetryableError(error) && i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
continue;
}
throw error;
}
}
throw lastError;
}
function isRetryableError(error) {
const retryableCodes = ['NETWORK_ERROR', 'TIMEOUT', 'SERVICE_UNAVAILABLE'];
return retryableCodes.includes(error.code);
}
const resolvers = {
Query: {
externalData: async () => {
return retryOperation(async () => {
return await externalAPI.fetchData();
});
}
}
};
7. Error Type Design
Error Result Types
type Error {
code: String!
message: String!
field: String
details: String
}
type UserResult {
user: User
errors: [Error!]!
success: Boolean!
}
type Mutation {
createUser(input: CreateUserInput!): UserResult!
updateUser(id: ID!, input: UpdateUserInput!): UserResult!
}
Implementation
const resolvers = {
Mutation: {
createUser: async (_, { input }) => {
const errors = [];
// Validate input
if (!input.name) {
errors.push({
code: 'REQUIRED_FIELD',
message: 'Name is required',
field: 'name'
});
}
if (!input.email) {
errors.push({
code: 'REQUIRED_FIELD',
message: 'Email is required',
field: 'email'
});
} else if (!isValidEmail(input.email)) {
errors.push({
code: 'INVALID_EMAIL',
message: 'Invalid email format',
field: 'email'
});
}
// If there are errors, return error information
if (errors.length > 0) {
return {
user: null,
errors,
success: false
};
}
// Create user
try {
const user = await User.create(input);
return {
user,
errors: [],
success: true
};
} catch (error) {
return {
user: null,
errors: [{
code: 'INTERNAL_ERROR',
message: 'Failed to create user',
details: error.message
}],
success: false
};
}
}
}
};
8. Error Monitoring and Alerting
Error Monitoring Integration
const Sentry = require('@sentry/node');
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV
});
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
{
requestDidStart: () => ({
didEncounterErrors: (context) => {
context.errors.forEach((error) => {
// Send error to Sentry
Sentry.captureException(error, {
tags: {
graphql: true,
operation: context.request.operationName
},
extra: {
query: context.request.query,
variables: context.request.variables
}
});
});
}
})
}
]
});
Error Alerting
const alertThreshold = {
errorRate: 0.05, // 5% error rate
errorCount: 100 // 100 errors
};
let errorCount = 0;
let requestCount = 0;
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
{
requestDidStart: () => ({
willSendResponse: (context) => {
requestCount++;
if (context.response.errors && context.response.errors.length > 0) {
errorCount += context.response.errors.length;
// Check if alerting is needed
const errorRate = errorCount / requestCount;
if (errorRate > alertThreshold.errorRate ||
errorCount > alertThreshold.errorCount) {
sendAlert({
message: 'High error rate detected',
errorRate,
errorCount,
requestCount
});
}
}
}
})
}
]
});
9. Error Handling Best Practices Summary
| Practice | Description |
|---|
| Use custom error classes | Create clear error hierarchy |
| Provide detailed error information | Include error codes, messages, and context |
| Partial error handling | Allow partially successful operations |
| Error formatting | Unified error response format |
| Error logging | Log all errors for analysis |
| Error recovery | Implement fallback and retry mechanisms |
| Error monitoring | Real-time error rate monitoring |
| Error alerting | Timely team notification |
10. Common Error Scenarios and Handling
| Scenario | Error Type | Handling |
|---|
| Resource not found | NotFoundError | Return 404 error |
| Validation failed | ValidationError | Return field-level errors |
| Authentication failed | AuthenticationError | Return 401 error |
| Authorization failed | AuthorizationError | Return 403 error |
| Data conflict | ConflictError | Return 409 error |
| Rate limit exceeded | RateLimitError | Return 429 error |
| Network error | NetworkError | Retry or fallback |
| Service unavailable | ServiceUnavailableError | Use cache or fallback |