Back to Blog
Lesson 44 of the GraphQL: Your First GraphQL Schema & Server course
API ArchitectureAugust 31, 20263 min read

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.

GraphQLAPIerror-handlingdebuggingnodejsbackend
Close-up of PHP code on a monitor, highlighting development and programming concepts.

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:

JAVASCRIPT
import { 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.

JAVASCRIPT
const 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

  1. Create a errors.js file in your project.
  2. Define a throwAuthenticationError function that sets a 401 status code and a UNAUTHENTICATED error code.
  3. Import this function into your Query resolver for a protected field.
  4. Trigger the error in the Apollo Sandbox and inspect the errors array in the response pane. Note how the extensions object appears in the output.

Common Pitfalls

  • Leaking Sensitive Info: Never pass database connection strings or raw stack traces into the message or extensions fields. 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.status field 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.

Similar Posts