Error Extensions
A message is for a human reading a log. But a client cannot reliably branch on a sentence — strings get reworded, translated, and reformatted. To let clients react programmatically to a failure, GraphQL gives every error an extensions map: a free-form, server-defined object hanging off the error entry. This is where typed errors live.
The extensions map
Section titled “The extensions map”extensions is the one error key the specification leaves open for you to fill. The near-universal convention is to put a stable code string in it:
{ "errors": [ { "message": "No track exists with that id.", "path": ["track"], "extensions": { "code": "NOT_FOUND" } } ]}The message may change over time, but extensions.code is a contract: "NOT_FOUND", "UNAUTHENTICATED", "FORBIDDEN", "BAD_USER_INPUT". A client switches on the code, not the prose. You can attach anything else you find useful too — a retry hint, a field name, a correlation id — as long as it is safe to expose.
Throwing a typed error
Section titled “Throwing a typed error”The reference implementation exports a GraphQLError class. Throw an instance of it from a resolver and pass your extensions in its options, and the engine threads them straight into the response:
import { GraphQLError } from 'graphql';
throw new GraphQLError('No track exists with that id.', { extensions: { code: 'NOT_FOUND' },});Compare that to throwing a plain Error. A plain Error still produces a valid error entry — you saw that in earlier lessons — but it has no code, so clients are left parsing the message. Reaching for GraphQLError is how you make a failure part of the typed contract instead of an accident of wording.
Masking internal errors
Section titled “Masking internal errors”There is a security angle. An unexpected exception — a failed database call, a null dereference deep in your code — often carries a stack trace or an internal message you must never ship to a client. The defensive pattern: let expected failures throw GraphQLError with a deliberate code, and catch everything else, replacing it with a generic masked error before it leaves the server.
flowchart TD
Throw["Resolver throws"]
Check{"Is it a deliberate GraphQLError?"}
Keep["Expected: keep message + code (NOT_FOUND, FORBIDDEN, ...)"]
Mask["Unexpected: log server-side, return generic INTERNAL_SERVER_ERROR"]
Client["Client reads extensions.code"]
Throw --> Check
Check -->|"yes"| Keep
Check -->|"no"| Mask
Keep --> Client
Mask --> Client In production you typically log the real exception server-side (with a correlation id) and return the client only a bland { message: "Internal server error", extensions: { code: "INTERNAL_SERVER_ERROR" } }. The user gets enough to retry or report; an attacker learns nothing about your internals. Many GraphQL servers do this masking for you by default; the principle is the same whether the framework or your own catch block does it.
A typed error, run for real
Section titled “A typed error, run for real”The runner below throws a GraphQLError with extensions: { code: 'NOT_FOUND' } when a track is missing. Press Run and inspect the error entry: it carries your code right alongside the message and path.
In the output, data.track is null because the field is nullable, and the single error entry now carries extensions.code === "NOT_FOUND" plus the argument hint we attached. A client can read that code and render “track not found” without ever parsing the English message — exactly the programmatic contract a plain Error could not give us.