GraphQL Error Handling â Error Patterns and Best Practices
In this tutorial, you will learn about Graphql Error Handling. We cover key concepts, practical examples, and best practices to help you master this topic.
GraphQL error handling differs from REST â errors are returned alongside data in a partial success model, requiring both client and server to handle errors gracefully.
What You'll Learn
You will learn Apollo Server error types, the partial success model, custom error codes, error masking in production, client-side error handling, and structured error responses.
Why Error Handling Matters
REST APIs return errors as HTTP status codes. GraphQL always returns 200 â errors are embedded in the response body. This enables partial success: one resolver might fail while others succeed, and the client gets both data and errors. DodaTech's Durga Antivirus Pro dashboard handles partial success gracefully â if the threat feed is down but device data is available, the dashboard shows devices with a "threat feed unavailable" banner instead of a blank screen.
flowchart TB
A["GraphQL Response"] --> B["data: { threats: [...], user: null }"]
A --> C["errors: [{ message: 'User not found', path: ['user'] }]"]
D["Client checks"] --> E["errors present?"]
E -->|"Yes"| F["Show warning\nbut render data"]
E -->|"No"| G["Render normally"]
style A fill:#dbeafe,stroke:#2563eb
style C fill:#fca5a5,stroke:#dc2626
Prerequisites: GraphQL mutations and resolvers.
Apollo Server Error Types
const {
ApolloError,
AuthenticationError, // 401 â Not authenticated
ForbiddenError, // 403 â Not authorized
UserInputError, // 400 â Invalid input
ValidationError, // 422 â Validation failed
} = require('apollo-server');
const resolvers = {
Mutation: {
createDevice: (_, { input }, context) => {
// Authentication check
if (!context.user) {
throw new AuthenticationError('You must be logged in');
}
// Authorization check
if (context.user.role !== 'ADMIN') {
throw new ForbiddenError('Only admins can create devices');
}
// Input validation
if (!input.name || input.name.length < 2) {
throw new UserInputError('Device name must be at least 2 characters', {
invalidArgs: ['name'],
});
}
// Business validation
if (input.name.includes('<script>')) {
throw new ValidationError('Device name contains invalid characters');
}
return createDevice(input);
},
},
};
// Response for UserInputError
{
"data": null,
"errors": [
{
"message": "Device name must be at least 2 characters",
"extensions": {
"code": "BAD_USER_INPUT",
"invalidArgs": ["name"]
}
}
]
}
Custom Error Codes
const { ApolloError } = require('apollo-server');
// Define custom error class
class DeviceOfflineError extends ApolloError {
constructor(message, deviceId) {
super(message, 'DEVICE_OFFLINE', { deviceId });
Object.defineProperty(this, 'name', { value: 'DeviceOfflineError' });
}
}
class ThreatNotFoundError extends ApolloError {
constructor(threatId) {
super(`Threat ${threatId} not found`, 'THREAT_NOT_FOUND', { threatId });
}
}
class RateLimitError extends ApolloError {
constructor(retryAfter) {
super('Too many requests', 'RATE_LIMITED', { retryAfter });
}
}
// Usage
const resolvers = {
Mutation: {
scanDevice: async (_, { id }, context) => {
const device = await context.db.devices.findById(id);
if (!device) {
throw new UserInputError('Device not found', { invalidArgs: ['id'] });
}
if (device.status === 'OFFLINE') {
throw new DeviceOfflineError('Device is offline and cannot be scanned', id);
}
if (context.rateLimiter.exceeded()) {
throw new RateLimitError(60);
}
return startScan(device);
},
},
};
Error Masking in Production
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (formattedError, error) => {
// In production, mask internal error details
if (process.env.NODE_ENV === 'production') {
// Don't expose stack traces or internal messages
if (formattedError.extensions?.code === 'INTERNAL_SERVER_ERROR'
|| !formattedError.extensions?.code) {
return {
message: 'An unexpected error occurred',
extensions: { code: 'INTERNAL_SERVER_ERROR' },
};
}
}
// Always include error code if present
return {
message: formattedError.message,
extensions: {
code: formattedError.extensions?.code || 'UNKNOWN',
...(formattedError.extensions?.invalidArgs && {
invalidArgs: formattedError.extensions.invalidArgs,
}),
},
};
},
});
Partial Success â Client Handling
// Client-side error handling
const query = gql`
query DashboardData {
user(id: "user-001") { name email }
threats { id name severity }
devices { id name }
}
`;
const { data, error } = await client.query({ query });
// Check for partial success
if (error?.graphQLErrors?.length) {
// Some resolvers failed, but data is still available
for (const err of error.graphQLErrors) {
console.error(`Error at path: ${err.path?.join('.')}`);
console.error(`Message: ${err.message}`);
}
}
// Render what we have
if (data?.user) renderUser(data.user);
if (data?.threats) renderThreats(data.threats);
if (data?.devices) renderDevices(data.devices);
Structured Error Response Pattern
// Return errors in data for better client handling
type DeviceResult {
success: Boolean!
device: Device
errors: [DeviceError!]
}
type DeviceError {
field: String!
message: String!
code: String!
}
type Mutation {
createDevice(input: CreateDeviceInput!): DeviceResult!
}
const resolvers = {
Mutation: {
createDevice: async (_, { input }, context) => {
const errors = [];
if (!input.name) errors.push({ field: 'name', message: 'Required', code: 'REQUIRED' });
if (input.name && input.name.length < 2) errors.push({ field: 'name', message: 'Too short', code: 'MIN_LENGTH' });
if (!input.os) errors.push({ field: 'os', message: 'Required', code: 'REQUIRED' });
if (errors.length > 0) {
return { success: false, device: null, errors };
}
const device = await context.db.devices.create(input);
return { success: true, device, errors: [] };
},
},
};
Common Mistakes
1. Throwing Generic Errors
throw new Error('Something went wrong') returns an INTERNAL_SERVER_ERROR with no useful information. Use typed Apollo errors.
2. Exposing Stack Traces in Production
Default Apollo Server includes stack traces in error extensions. Use formatError to mask them in production.
3. Ignoring Partial Success
Clients should always check for both data and errors. Rendering only when data exists ignores partial success.
4. Not Setting Error Codes
Without extensions.code, clients cannot programmatically handle errors. Always set a specific error code.
5. Throwing Errors in Subscription Resolvers
Subscription errors close the Websocket connection. Handle errors gracefully in subscription resolvers and send error messages as events instead.
Practice Questions
- How does GraphQL error handling differ from REST?
- What Apollo Server error types are available?
- How do you create custom error codes?
- What does formatError do?
- How should clients handle GraphQL errors?
Answers:
- GraphQL always returns HTTP 200 with errors in the response body (
errorsarray alongsidedata). REST uses HTTP status codes. GraphQL supports partial success. AuthenticationError(401),ForbiddenError(403),UserInputError(400),ValidationError(422), and genericApolloErrorfor custom errors.- Create a class extending
ApolloErrorwith a unique error code string:class MyError extends ApolloError { constructor() { super(message, 'MY_CODE'); } }. formatErroris an Apollo Server option that transforms errors before sending the response. Use it to mask internals, add consistent formatting, or log errors.- Always check both
dataanderrors. Render available data, show warnings for failed sections, and never assume full success or full failure.
Challenge: Design a comprehensive error handling system for DodaTech's GraphQL API. Include custom error classes for each domain (DeviceOfflineError, ScanInProgressError, ThreatAlreadyResolvedError), structured error responses for mutations, error masking in production, and client-side handling that shows per-section error states in the dashboard.
FAQ
Mini Project
Build a GraphQL error handling system for DodaTech. Include domain-specific error classes, a formatError function that masks internals in production, mutation result types with structured errors, and a React ErrorBoundary component that renders per-section error states for the dashboard.
What's Next
| Topic | Description |
|---|---|
| Authentication | Auth in GraphQL APIs |
| Authorization | Role-based access control |
| Security | Depth limiting, cost analysis |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro