Advanced Error Handling: Custom Classes and GraphQLError
Learn how to use the GraphQLError class to build meaningful, structured API responses. Move beyond basic errors to provide actionable feedback to your users.

Previously in this course, we discussed Handling Mutation Errors: Throwing and Returning GraphQL Errors, where we covered the basics of stopping execution when inputs fail validation.
In this lesson, we are leveling up. Instead of throwing generic strings, we will use the GraphQLError class to create custom, typed error responses. This allows your client applications to programmatically react to specific failure modes—like distinguishing between a "User Not Found" error and a "Permission Denied" error—without parsing text strings.
The Problem with Generic Errors
When you throw a standard JavaScript Error in a resolver, the GraphQL server captures it and masks it as a generic INTERNAL_SERVER_ERROR. This is great for security (you don't want to leak stack traces to the public), but it's terrible for the developer experience on the client side.
By utilizing the GraphQLError class, we can attach extensions to our errors. These are key-value pairs that carry metadata, such as error codes or specific fields that failed validation, allowing the client to handle the error contextually.
Creating Custom Error Types
To maintain a clean codebase, I recommend creating a dedicated error factory or a set of classes that extend GraphQLError. This keeps your resolvers focused on business logic rather than error formatting.
Here is how you implement a custom error structure:
JAVASCRIPTimport { GraphQLError } from CE9178">'graphql'; // A factory function to generate a specific "Not Found" error export const throwNotFoundError = (message, resourceId) => { throw new GraphQLError(message, { extensions: { code: CE9178">'NOT_FOUND', http: { status: 404 }, resourceId, }, }); }; // A factory for "Bad Request" errors(e.g., validation) export const throwValidationError = (message, invalidArgs) => { throw new GraphQLError(message, { extensions: { code: CE9178">'BAD_USER_INPUT', http: { status: 400 }, invalidArgs, }, }); };
Integrating into Resolvers
Now, let's update our updateUser mutation from our running project. Instead of just returning null or throwing a generic error, we provide specific metadata.
JAVASCRIPTconst resolvers = { Mutation: { updateUser: (_, { id, email }, { dataSources }) => { const user = dataSources.users.findById(id); if (!user) { throwNotFoundError(CE9178">'User not found', id); } if (!email.includes(CE9178">'@')) { throwValidationError(CE9178">'Invalid email format', [CE9178">'email']); } return dataSources.users.update(id, { email }); }, }, };
Why This Matters for Debugging
When a client receives this response, the extensions field provides the code and the invalidArgs array. A frontend developer can now use this to automatically highlight the email input field in red or redirect the user to a "404" page, rather than showing a generic "Something went wrong" toast notification.
Hands-on Exercise
- Create a
errors.jsfile in your project. - Define a
throwAuthenticationErrorfunction that sets a401status code and aUNAUTHENTICATEDerror code. - Import this function into your
Queryresolver for a protected field. - Trigger the error in the Apollo Sandbox and inspect the
errorsarray in the response pane. Note how theextensionsobject appears in the output.
Common Pitfalls
- Leaking Sensitive Info: Never pass database connection strings or raw stack traces into the
messageorextensionsfields. Always sanitize your error messages. - Over-complicating: Don't create a custom class for every single possible error. Stick to a few high-level categories (e.g.,
NOT_FOUND,BAD_USER_INPUT,FORBIDDEN). - Ignoring HTTP Status: While GraphQL is transport-agnostic, most Apollo implementations respect the
http.statusfield in extensions. Use it to help your infrastructure (like load balancers or monitoring tools) understand the nature of the failure.
For a broader look at designing professional, standardized error interfaces across your entire stack, check out Error Handling Best Practices: Clean API Design and Debugging. This will help you keep your API design consistent as you scale.
Recap
We've moved from basic error handling to a structured approach using GraphQLError. By injecting metadata through the extensions field, you provide your API consumers with actionable data, enabling them to build better user interfaces.
Up next, we will look at Schema Stitching Basics, where we explore how to combine multiple GraphQL services into one unified gateway.
Work with me

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.

Custom WordPress Plugin Development
Custom WordPress & WooCommerce plugins built to standard — by the developer behind a plugin with 5,000+ active installs and a SaaS with 10,000+ users.


