Error Handling & Validation

GraphQL — Error Handling & Validation
GraphQL
Chapter 7 · Error Handling & Validation

🚨 Error Handling & Validation

Chapter 6 built a working server. This chapter covers what happens when something goes wrong — and GraphQL's approach is genuinely different from the HTTP status codes REST relies on.

Errors Don't Change the HTTP Status

A GraphQL response typically returns HTTP 200 even when a resolver throws — errors are reported in a separate errors array in the response body, alongside a data field that might still be partially populated:

{ "data": { "user": { "name": "Alice", "posts": null } }, "errors": [ { "message": "Failed to load posts", "path": ["user", "posts"], "locations": [{ "line": 3, "column": 5 }] } ] }

Because a query touches many resolvers, some fields can succeed while others fail in the very same response. If Post's title field were non-null (String!, per Chapter 2) and its resolver threw, that null would propagate up to the nearest nullable parent instead — GraphQL's execution guarantees a non-null field is never actually returned as null.

The Built-In Error Shape

FieldWhat It Is
messageHuman-readable description of what went wrong
locationsLine/column in the original query where the failing field was requested
pathThe exact field path that failed — matches Chapter 3's resolver-nesting structure

🏷️ Custom Errors: Conveying a Category

GraphQL has no equivalent to HTTP's 404/401/500 — instead, an extensions.code convention plays that role:

Throwing a Custom Error

import { GraphQLError } from "graphql"; Query: { user: async (parent, args, context) => { const user = await context.userLoader.load(args.id); if (!user) { throw new GraphQLError("User not found", { extensions: { code: "NOT_FOUND" }, }); } return user; }, }

Conveying Error Category

REST

The HTTP status code itself carries the category — 404, 401, 400, 500.

GraphQL

Status stays 200; category lives in extensions.code instead — NOT_FOUND, UNAUTHENTICATED, BAD_USER_INPUT.

Input Validation Is a Separate Concern

The schema's ! only guarantees a field is present and the right type — not that a string isn't empty, or a number is in a sensible range. That's the same "never trust input" lesson from the site's security courses, applied to mutation arguments before a write happens:

createPost: (parent, args, context) => { if (args.input.title.trim().length === 0) { throw new GraphQLError("Title cannot be empty", { extensions: { code: "BAD_USER_INPUT" }, }); } return db.posts.create(args.input); }

📦 The Payload Pattern, Revisited

Chapter 4 mentioned wrapping mutation results in a payload type. That pattern is often preferred specifically for expected, recoverable validation errors, keeping the top-level errors array reserved for unexpected, systemic failures:

Top-Level errors Array

Unexpected, systemic problems — a database connection failure, a bug. The client generally can't do anything meaningful except show a generic failure state.

Payload-Level errors Field

Expected, user-facing validation problems — "title too short," "email already taken." The client can show these directly next to the relevant form field.

💻 Coding Challenges

Challenge 1: Throw an UNAUTHENTICATED Error

Write a resolver for a hypothetical deletePost mutation that throws a GraphQLError with extensions.code: "UNAUTHENTICATED" if context.currentUser is missing, before performing the delete.

Goal: Practice throwing a categorized error before a resolver's main logic runs.

→ Solution

Challenge 2: Design a Payload Type With Errors

Define a CreatePostPayload type with post: Post and errors: [UserError!]! fields (where UserError { field: String!, message: String! }), and change createPost's return type to it.

Goal: Practice the payload-wrapper pattern for surfacing expected validation errors alongside a possibly-null result.

→ Solution

Challenge 3: Explain a Client Bug

A client's code does if (response.status === 200) { showSuccess(); } after every GraphQL mutation, without ever inspecting the response body's errors array. Explain what's wrong with this and what the client should do instead.

Goal: Practice recognizing the exact misconception this chapter's tip box addresses.

→ Solution

⚠️ Gotcha: Checking HTTP Status Instead of the Response Body

Client code trained on REST habitually checks response.ok or status === 200 as a proxy for "the operation succeeded" — for GraphQL, that check is meaningless. A 200 response with a populated errors array is a completely normal, expected shape, not an edge case. Every GraphQL client must inspect the response body's errors array directly, every time, regardless of HTTP status — treating a 200 status alone as proof of success will silently miss real, reported failures sitting right there in the same response.

🎯 What's Next

With errors handled, the next chapter covers who's allowed to do what: Authentication & Authorization in GraphQL — context-based auth, and field-level authorization.