Class 12: Guidelines: A Decision Framework
Duration: 15 minutes | Difficulty: Intermediate | Prerequisites: This class synthesizes Classes 1 through 11 and cites them throughout; it can be read first as a preview or last as the summary it is meant to be.
What You'll Learn
By the end of this class, you will:
- Walk any error, real or hypothetical, through one decision path from occurrence to every consumer's reaction
- Have the course's guidelines as twenty one-sentence rules, each with its argument one click away
- Sequence a realistic migration for an existing schema, from codes to semantic nullability
- Know where every primary source in this course lives, organized for reference
The decision path
Something just went wrong, or produced an outcome the client did not hope for, inside one GraphQL request. Walk it through five questions.
Question 1: Could this happen to a well-behaved client on a healthy system, as a consequence of what the user asked for? This is Class 5's line, and it is the first question because it separates the two channels everything else assumes. If yes, it is an expected business outcome: it belongs in the schema, as data, and continues to Question 2. If no, it is a failure: it belongs in the errors list, and skips to Question 3.
Question 2: How does the outcome enter the schema? As a typed result the client cannot miss. The shape from Class 5: the mutation returns a ...Response type carrying the entity (null on failure) and a userErrors list of a union whose members implement a shared error interface. Each member carries message, a machine-readable field path into the input (Class 9's Shopify lesson), and whatever outcome-specific fields the client needs (maxLength, a suggestion, an index). On a failed update, return the current true state of the entity alongside the errors, GitLab's rule from Class 10. The resolver returns these as values; per Class 6, outcomes never ride the exception channel. The response then counts as a success everywhere else: 200 per Class 3, errors absent, and monitoring counts it as a normal response.
Question 3: Which failure quadrant is it? Class 4's two axes decide it: whose fault, and does retry help.
- A client mistake, meaning a bad input shape, an unknown field or an unparseable document, is the client's to fix.
- An identity problem is
UNAUTHENTICATEDorPERMISSION_DENIED, kept distinct because clients react differently: a login prompt for the first, an access-denied message for the second. - A transient system failure is
UNAVAILABLE, ideally withretryAfterSeconds. Rate limiting gets its own retry-safe code with a delay. - A persistent system failure is
INTERNAL: masked, logged, and alerting the owning team.
The failure becomes a GraphQLError whose message is developer-facing text, and whose classification and stable code live in extensions. Class 1's rule keeps them out of additional keys placed directly on the error, the placement Class 10 showed GitHub using. Those extensions carry a correlation reference on INTERNAL (Class 6), and stack traces, class names and infrastructure detail stay on the server (Class 4).
Question 4: What does the transport say? The status-code rules from Class 3, which depend on the response body:
- Request never executed (no
dataentry): the specific4xx, or5xxwhen the server is at fault, with503for load-shedding.400covers parse failures, and422covers a request that is not well-formed, validation failures, an operation that cannot be determined, and variable values of the wrong type.401and403cover permission, and the method and size codes apply where relevant. - Executed cleanly:
200. - Executed with errors alongside data (even fully nulled data):
294underapplication/graphql-response+json, the April 2026 recommendation, so intermediaries see partial success without parsing bodies.
A request error result is still a well-formed GraphQL-over-HTTP response and ships as application/graphql-response+json; only a reply sent because an error prevented a well-formed GraphQL-over-HTTP response must avoid that media type. Two rules are MUST strength: a 2xx when data is present and non-null, and some 4xx or 5xx when the data entry is absent; the choice among the specific codes is SHOULD strength.
Question 5: What does each consumer do? For the client, from Class 7:
- Configure the library to receive partial data and errors together: Apollo
all, not the defaultnone. - Branch on codes, never on messages.
- Route
UNAUTHENTICATEDto re-authentication andUNAVAILABLEto bounded retry, through one cross-cutting link. - Render failures as section-scoped error messages behind boundaries.
- Keep error nulls out of the normalized store.
For monitoring:
- Alert on
INTERNALrates. - Watch
422andBAD_USER_INPUTafter deploys. - Chart
294against200.
Business outcomes stay out of these numbers, because Question 2 kept them out of the failure channels. For support, take the reference from the user's screenshot to the log line with the stack trace.
The guidelines
Each rule is one sentence; the linked class carries the argument and the primary sources.
Channels
- Expected outcomes of user input go in the schema as typed results; failures go in the
errorslist, and the deciding test is whether it could happen on a healthy system (Class 5). - Auth failures, rate limits, and infrastructure problems stay out of the schema (Class 5).
- Write the placement rule down and enforce it in review, because convention without policy drifts (Class 10).
Errors
messageis developer-facing text that client code must never branch on (Class 4).- Machine-readable classification lives in
extensions, never as additional keys outsideextensions(Class 1). - Keep the code vocabulary small, closed, documented, and evolved like schema (Class 4).
- Mark the retry-safe codes explicitly (
UNAVAILABLE, and a rate-limit code if you have one), and apply them only to failures that a retry can fix (Class 4). - Ship a correlation reference on masked internal errors, and keep every other internal detail on the server (Class 6).
Schema outcomes
- Give outcome types machine-readable
fieldpaths so form UIs never parse message text (Class 9). - Put a shared interface under typed outcome unions so old clients survive new outcomes (Class 5).
- Return the entity's true state alongside errors on failed updates (Class 10).
- Enforce "every union member implements the interface" with a linter or schema test, because nothing else will (Class 5).
Server
- Mask by default; an unrecognized exception is
INTERNALwith an opaque message, logged in full (Class 6). - Translate exceptions to classified errors at one point (resolver chain or advice) shared by all resolvers (Class 6).
- Cover the odd corners: per-key batch failures, a subscription exception resolver, and, in a federated graph, a deliberate router error-inclusion policy (Class 6).
Transport
- Support
application/graphql-response+jsonand use the draft's status codes;200clean,294partial, specific4xxunexecuted,5xxserver-fault (Class 3).
Client
- Receive partial data and errors together, and render both honestly at section granularity (Class 7).
- Centralize the reactions to error codes (re-auth, retry) in one link or interceptor (Class 7).
- Never let an error null be written to shared state as if it were a value (Class 7).
Direction
- Declare fields the domain always populates with
@semanticNonNull, and trackonErroras it moves toward the spec (Class 8).
Migrating an existing schema
Nobody applies twelve classes to a running API at once. The sequence below is ordered so every step is additive and delivers its benefit before the next begins:
- Codes. Start here, because everything downstream branches on them. Adopt a Class 4 vocabulary in
extensions.codethrough the Class 6 single translation point, and leave messages untouched so existing clients keep working. Wire the codes into monitoring the same week, which is when the benefit becomes visible. - Transport. Adopt the new media type and status codes via content negotiation, which is invisible to legacy clients and immediately legible to your infrastructure. Spring for GraphQL already sends the new media type and the
4xxcodes, and294is pending there (issue #1314), so a Spring server keeps sending200for partial results until that lands. - Outcomes, new surface first. Apply the Class 5 shape to new mutations only, because retrofitting every existing mutation at once is a large project that is easy to abandon. Migrate old mutations opportunistically, when they are touched anyway, with the interface pattern from Class 9 making the migration gradual.
- Clients. Switch error policies to receive partial data, centralize the reactions to error codes, and add boundaries. This step can start the day codes exist.
- Nullability. Annotate truly-never-absent fields with
@semanticNonNull, which is additive and safe, per Class 8. Derive strict and nullable SDL views withgraphql-sockwhere tooling needs them. Then revisit the defensive nullables in new types, thinking about the domain instead of the error path.
The onError step is still ahead of production use. The specification work sits in three open proposals:
- Error behaviors, in PR #1163.
- Service capabilities, in PR #1208.
- The
onErrorrequest property, in PR #1236.
graphql-js v17 and graphql-java v23.0 and later ship an experimental @experimental_disableErrorPropagation directive with that non-propagating behavior, if you want to exercise it in a test environment first.
Summary
One decision path covers every case:
- Outcomes of user input on a healthy system go into the schema as typed, field-addressed results.
- Everything else becomes a classified error. Its code answers "whose fault, and does retry help", its transport status makes the body's situation legible to infrastructure, and each of its consumers has one prescribed reaction.
- Twenty guidelines compress the course, from the channel split through masking,
294, section-scoped rendering, and cache hygiene, to@semanticNonNullas the standing recommendation. - Migration runs codes, transport, outcomes-on-new-surface, clients, then nullability, each step additive.
The specification work of Class 8 aims at the root conflation itself, where earlier efforts addressed its symptoms, and the master reading list below collects the course's sources, checked on 22 September 2026.
Master Further Reading
The course's primary sources, organized by part, each annotated once.
Foundations
- The GraphQL spec's Errors section (the error model itself, a short read) and Handling Execution Errors (the propagation rules).
- The GraphQL-over-HTTP draft with its Status Codes section and PR #346 (the
294decision and its reasoning). - graphql.org on nullability, error handling, and serving over HTTP (the official positions, each a page).
Design
- Apollo Server's error docs and Netflix DGS's error docs (the two published vocabularies).
- Giroux's A Guide to GraphQL Errors (the design ladder) and Solomon's 200 OK! (the philosophy under it).
- Spring for GraphQL's exception documentation (the server mechanics of Class 6).
Client and future
- Apollo Client error handling (policies and the link) and urql's errors page (
CombinedError). - Relay's @required, @catch, @throwOnFieldError, and semantic nullability guides (the strictest shipping model).
- The true-nullability draft blog post, still unpublished, and the live RFCs #1163, #1208 and #1236.
- The @semanticNonNull spec, graphql-sock, and the nullability working group archive (the whole future arc with its history).
Case studies
- Shopify's UserError, DisplayableError, and MetafieldsSetUserError references (the pattern and its evolution, from the source).
- GitLab's GraphQL style guide (the written placement rules).
- GitHub's rate and query limits alongside docs issue #22607 (what is specified, and the documented gap).
- Eldredge's GraphQLConf 2024 talk and the nullability panel (the origin story, recorded).
What's Next
A future update to this course will add a runnable companion: the Movie Database wired with every server pattern from Class 6 and a client demonstrating Class 7's policies side by side. Until then, the Spring GraphQL tutorial carries the hands-on server half, our frontend tutorial (coming soon) will carry the client half, and the decision path above is ready for your next schema review.