Back to Blog
Lesson 33 of the GraphQL: Your First GraphQL Schema & Server course
API ArchitectureAugust 20, 20264 min read

Handling Mutation Errors: Throwing and Returning GraphQL Errors

Learn how to implement effective error handling in GraphQL mutations. Discover how to throw errors for invalid inputs and return meaningful messages to clients.

GraphQLAPI ArchitectureError HandlingMutationsBackend Development
Simple and minimalist image showcasing the word 'ERROR' on a white background.

Previously in this course, we covered Managing Mutation State: Updating Arrays in GraphQL Resolvers. Now that we can successfully modify our data, we need to ensure those modifications are safe. This lesson adds robust error handling to your mutations, ensuring your API communicates failures clearly rather than failing silently or crashing.

Why Error Handling Matters

In a standard REST API, you rely on HTTP status codes like 400 Bad Request or 422 Unprocessable Entity to signal problems. Because GraphQL operates over a single endpoint—typically returning a 200 OK status even when a request fails—you must use the GraphQL errors array to communicate issues to the client.

If a user attempts to update an item that doesn't exist or provides invalid input, your resolver shouldn't just return null or throw a generic server exception. It should explicitly inform the client why the operation failed.

Throwing Errors from Resolvers

In Apollo Server, you can import the GraphQLError class. When you throw this error inside a mutation, the server catches it automatically and formats it into the standard GraphQL response structure.

Here is a concrete example of a mutation that updates a user's email. We want to prevent the mutation if the email is already taken or if the format is invalid.

JAVASCRIPT
import { GraphQLError } from CE9178">'graphql';

const resolvers = {
  Mutation: {
    updateUserEmail: (_, { id, newEmail }, { users }) => {
      const user = users.find(u => u.id === id);

      if (!user) {
        throw new GraphQLError(CE9178">'User not found', {
          extensions: {
            code: CE9178">'USER_NOT_FOUND',
            http: { status: 404 },
          },
        });
      }

      if (!newEmail.includes(CE9178">'@')) {
        throw new GraphQLError(CE9178">'Invalid email format', {
          extensions: {
            code: CE9178">'BAD_USER_INPUT',
            http: { status: 400 },
          },
        });
      }

      user.email = newEmail;
      return user;
    },
  },
};

Anatomy of a GraphQL Error

Notice the extensions object in the example above. This is where the magic happens. While the message field provides a human-readable description for the developer, the extensions object allows you to pass structured metadata:

  1. code: A machine-readable string (e.g., USER_NOT_FOUND) that your frontend can use to trigger specific UI logic, like showing a localized error message.
  2. http: While optional, providing an http object helps if you are logging errors through middleware that inspects status codes.

Hands-on Exercise

Open your current project and locate your "add" or "update" mutation resolver.

  1. Add a guard clause to check if the incoming arguments are valid (e.g., ensure a string isn't empty).
  2. If the validation fails, throw a new GraphQLError.
  3. Include a code field in the extensions object such as INVALID_INPUT.
  4. Restart your server and trigger that mutation in Apollo Sandbox with invalid data. Observe how the errors array appears in the response pane.

Common Pitfalls

  • Throwing raw JavaScript Errors: Avoid throwing standard Error objects. While they will be caught, they often expose stack traces to the client, which is a significant security risk. Always use GraphQLError.
  • Over-sharing details: Don't leak database connection errors or internal system paths in your error messages. Keep messages strictly related to the business logic failure.
  • Silent Failures: Don't return null for a failed mutation unless your schema explicitly allows nullability for that return type. If the mutation must return an object, an error is the only correct way to stop execution.

FAQ

Q: Should I handle all validation inside the resolver? A: For simple logic, yes. For complex validation, you might want to move this to a dedicated validation service layer to keep your resolvers thin.

Q: Can I return data and an error at the same time? A: Yes. If a field in a list fails, GraphQL can return partial data for the successful items and an error object for the failed one. However, for mutations, it's standard practice to halt execution if a critical business rule is violated.

Q: Does every error need an HTTP status code? A: Not strictly, as GraphQL defaults to 200. However, adding status codes via the extensions object is a best practice if you are using tools that monitor HTTP traffic logs.

Recap

By implementing proper error handling, you ensure your API is predictable and debuggable. We use GraphQLError to signal issues, provide machine-readable codes for frontend logic, and avoid leaking implementation details. This follows the principles of robust API development, similar to Handling API Errors: A Type-Safe Approach in TypeScript, where clear error communication is the foundation of a reliable system.

Up next: We will look at Designing for Deletion to safely remove resources from our API.

Similar Posts