GraphQL

Error Handling in GraphQL

18 min Lesson 11 of 35

Understanding GraphQL Error Handling

GraphQL has a standardized error format that allows partial data delivery alongside errors. Unlike REST APIs that return HTTP status codes, GraphQL responses always return 200 OK and include errors in the response body.

GraphQL Error Format

The standard GraphQL error structure includes:

{ "data": { "user": null }, "errors": [ { "message": "User not found", "locations": [{"line": 2, "column": 3}], "path": ["user"], "extensions": { "code": "USER_NOT_FOUND", "timestamp": "2026-02-16T10:30:00Z" } } ] }
Note: The data field can contain partial results even when errors occur, allowing clients to use whatever data was successfully resolved.

Custom Error Classes

Create custom error classes for better error handling:

// errors/CustomErrors.js class AuthenticationError extends Error { constructor(message) { super(message); this.extensions = { code: 'UNAUTHENTICATED', http: { status: 401 } }; } } class ForbiddenError extends Error { constructor(message) { super(message); this.extensions = { code: 'FORBIDDEN', http: { status: 403 } }; } } class ValidationError extends Error { constructor(message, fields) { super(message); this.extensions = { code: 'BAD_USER_INPUT', fields: fields, http: { status: 400 } }; } } class NotFoundError extends Error { constructor(message) { super(message); this.extensions = { code: 'NOT_FOUND', http: { status: 404 } }; } } module.exports = { AuthenticationError, ForbiddenError, ValidationError, NotFoundError };

Using Custom Errors in Resolvers

const { AuthenticationError, NotFoundError } = require('./errors/CustomErrors'); const resolvers = { Query: { user: async (_, { id }, context) => { if (!context.user) { throw new AuthenticationError('You must be logged in'); } const user = await User.findById(id); if (!user) { throw new NotFoundError(`User with ID ${id} not found`); } return user; } }, Mutation: { updateUser: async (_, { id, input }, context) => { if (!context.user) { throw new AuthenticationError('Authentication required'); } if (context.user.id !== id && !context.user.isAdmin) { throw new ForbiddenError('You can only update your own profile'); } return await User.findByIdAndUpdate(id, input, { new: true }); } } };

Error Masking and Security

Mask internal errors in production to prevent information leakage:

const { ApolloServer } = require('apollo-server'); const server = new ApolloServer({ typeDefs, resolvers, formatError: (error) => { // Log the original error console.error(error); // Don't expose internal errors to clients in production if (process.env.NODE_ENV === 'production') { // Check if it's a known error type if (error.extensions?.code) { return error; } // Mask unexpected errors return { message: 'Internal server error', extensions: { code: 'INTERNAL_SERVER_ERROR' } }; } // In development, return full error return error; } });
Warning: Never expose database errors, stack traces, or internal implementation details to clients in production. Always mask unexpected errors.

Partial Data with Errors

GraphQL allows returning partial data when some fields fail:

const resolvers = { Query: { dashboard: async (_, __, context) => { if (!context.user) { throw new AuthenticationError('Login required'); } return {}; // Return empty object, fields resolve independently } }, Dashboard: { profile: async (parent, _, context) => { try { return await User.findById(context.user.id); } catch (error) { console.error('Profile fetch failed:', error); return null; // Partial failure - other fields still resolve } }, posts: async (parent, _, context) => { try { return await Post.find({ userId: context.user.id }); } catch (error) { console.error('Posts fetch failed:', error); throw new Error('Failed to load posts'); } }, stats: async (parent, _, context) => { return await Stats.getForUser(context.user.id); } } }; // Query result with partial data: // { // "data": { // "dashboard": { // "profile": null, // "posts": null, // "stats": { "views": 1250, "followers": 42 } // } // }, // "errors": [ // { // "message": "Failed to load posts", // "path": ["dashboard", "posts"] // } // ] // }

Field-Level Error Handling

const resolvers = { User: { email: (parent, _, context) => { // Only show email to the user themselves or admins if (context.user?.id === parent.id || context.user?.isAdmin) { return parent.email; } throw new ForbiddenError('Email is private'); }, privateData: async (parent, _, context) => { if (context.user?.id !== parent.id) { throw new ForbiddenError('Cannot access private data'); } try { return await fetchPrivateData(parent.id); } catch (error) { console.error('Private data fetch failed:', error); throw new Error('Failed to load private data'); } } } };
Tip: Use error codes in the extensions field to allow clients to handle different error types programmatically without parsing error messages.

Apollo Server Error Formatting

const { ApolloServer } = require('apollo-server'); const { v4: uuidv4 } = require('uuid'); const server = new ApolloServer({ typeDefs, resolvers, formatError: (error) => { // Generate error ID for tracking const errorId = uuidv4(); // Enhanced logging console.error('GraphQL Error:', { errorId, message: error.message, code: error.extensions?.code, path: error.path, locations: error.locations, originalError: error.originalError }); // Add error ID to response return { ...error, extensions: { ...error.extensions, errorId, timestamp: new Date().toISOString() } }; }, context: async ({ req }) => { // Context can throw errors too const token = req.headers.authorization?.replace('Bearer ', ''); if (token) { try { const user = await verifyToken(token); return { user }; } catch (error) { throw new AuthenticationError('Invalid token'); } } return {}; } });
Exercise:
  1. Create custom error classes for RateLimitError and ConflictError
  2. Implement a resolver that throws different error types based on business logic
  3. Add error logging with error IDs that can be referenced by clients
  4. Create a formatError function that masks stack traces in production
  5. Test partial data delivery by making one field throw an error while others succeed