Error Handling & Validation
🚨 Error Handling & Validation
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:
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
| Field | What It Is |
|---|---|
message | Human-readable description of what went wrong |
locations | Line/column in the original query where the failing field was requested |
path | The 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
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:
📦 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.
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.
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.
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.