5月28日 09:24
What are the best practices for GraphQL error handling
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
Basic Error Response Format
json{ "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
javascriptclass 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
javascriptconst 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
javascriptconst 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 }; } } };
4. Error Formatting
Custom Error Formatter
javascriptconst 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
javascriptconst 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
javascriptconst 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
javascriptasync 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
graphqltype 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
javascriptconst 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
javascriptconst 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
javascriptconst 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 |