GraphQL Error Handling

Error handling in GraphQL works differently from REST. Instead of using HTTP status codes like 404 or 500 to signal problems, GraphQL always returns HTTP 200 and puts error information inside the response body. Understanding the two types of errors and how to handle each one makes your API far more reliable and your clients easier to build.

Two Types of GraphQL Errors

  Type 1: System Errors                Type 2: Business Errors
  ─────────────────────                ───────────────────────
  Unexpected failures                  Expected application-level failures
  Server crash, DB down                Email taken, password too short
  Unhandled exceptions                 Insufficient funds, item out of stock
  Go in: top-level "errors" array      Go in: mutation payload data
  HTTP: still 200 OK                   HTTP: 200 OK
  Client: error-handling middleware    Client: reads payload.errors

The GraphQL Error Response Structure

  Partial success — some fields work, some fail:
  ───────────────────────────────────────────────
  {
    "data": {
      "user": {
        "name": "Vikram",
        "posts": null      ← posts resolver threw an error
      }
    },
    "errors": [
      {
        "message":   "Posts service is unavailable",
        "locations": [{ "line": 4, "column": 5 }],
        "path":      ["user", "posts"],
        "extensions": {
          "code": "SERVICE_UNAVAILABLE"
        }
      }
    ]
  }

Throwing Errors Inside Resolvers

Use GraphQL's built-in GraphQLError class to throw errors with structured metadata. The extensions object carries machine-readable error codes that clients can switch on.

  import { GraphQLError } from 'graphql';

  const resolvers = {
    Query: {
      user: async (_, args, ctx) => {
        const user = await ctx.db.users.findById(args.id);

        if (!user) {
          throw new GraphQLError('User not found', {
            extensions: {
              code:       'NOT_FOUND',
              userId:     args.id,
              httpStatus: 404,
            }
          });
        }

        return user;
      }
    }
  };

Standard Error Codes

  Code                    When to use
  ────                    ───────────
  UNAUTHENTICATED         No valid token — user not logged in
  FORBIDDEN               Logged in but lacks permission
  NOT_FOUND               Requested resource does not exist
  BAD_USER_INPUT          Client sent invalid argument values
  INTERNAL_SERVER_ERROR   Unexpected server-side failure
  RATE_LIMITED            Too many requests
  SERVICE_UNAVAILABLE     Downstream service is down

Business Errors in Mutation Payloads

Validation failures and business rule violations belong in the mutation payload, not in the top-level errors array. This approach makes client handling predictable and avoids mixing system errors with user-facing messages.

  type UserError {
    field:   String
    message: String!
  }

  type RegisterPayload {
    user:   User
    errors: [UserError!]!
  }

  // Resolver:
  register: async (_, { input }) => {
    const existing = await db.users.findByEmail(input.email);
    if (existing) {
      return {
        user: null,
        errors: [{
          field:   'email',
          message: 'An account with this email already exists'
        }]
      };
    }
    const user = await db.users.create(input);
    return { user, errors: [] };
  }

Error Masking in Production

Never expose internal error details (database errors, stack traces, file paths) to clients in production. Apollo Server masks unexpected errors automatically in production mode. Catch and rethrow database errors with safe, generic messages.

  // Development — full error detail:
  "message": "relation 'users' does not exist at character 15"

  // Production — safe message:
  "message": "Internal server error"

  // In resolver — catch and rethrow safely:
  try {
    return await db.users.findById(id);
  } catch (err) {
    console.error('DB error:', err);   // Log the real error server-side
    throw new GraphQLError('Failed to load user', {
      extensions: { code: 'INTERNAL_SERVER_ERROR' }
    });
  }

Key Points

  • GraphQL returns HTTP 200 for all responses — errors live in the response body, not the status code.
  • System errors go in the top-level errors array; business errors belong in mutation payload data.
  • Use GraphQLError with an extensions.code to give clients machine-readable error codes.
  • Mask internal error details in production — log the real error server-side only.
  • A response can contain both partial data and errors simultaneously, allowing partial success.

Leave a Comment