Skip to content

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.

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.

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.

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
Expected failures keep their code; unexpected ones are masked.

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.

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.

JavaScript

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.

What is the extensions map on an error entry used for?
Why should a client branch on extensions.code rather than on message?
How do you attach a typed code to an error from a resolver?
What is the recommended treatment of an unexpected internal exception in production?