Skip to main content

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 UNAUTHENTICATED or PERMISSION_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 with retryAfterSeconds. 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 data entry): the specific 4xx, or 5xx when the server is at fault, with 503 for load-shedding. 400 covers parse failures, and 422 covers a request that is not well-formed, validation failures, an operation that cannot be determined, and variable values of the wrong type. 401 and 403 cover permission, and the method and size codes apply where relevant.
  • Executed cleanly: 200.
  • Executed with errors alongside data (even fully nulled data): 294 under application/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 default none.
  • Branch on codes, never on messages.
  • Route UNAUTHENTICATED to re-authentication and UNAVAILABLE to 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 INTERNAL rates.
  • Watch 422 and BAD_USER_INPUT after deploys.
  • Chart 294 against 200.

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

  1. Expected outcomes of user input go in the schema as typed results; failures go in the errors list, and the deciding test is whether it could happen on a healthy system (Class 5).
  2. Auth failures, rate limits, and infrastructure problems stay out of the schema (Class 5).
  3. Write the placement rule down and enforce it in review, because convention without policy drifts (Class 10).

Errors

  1. message is developer-facing text that client code must never branch on (Class 4).
  2. Machine-readable classification lives in extensions, never as additional keys outside extensions (Class 1).
  3. Keep the code vocabulary small, closed, documented, and evolved like schema (Class 4).
  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).
  5. Ship a correlation reference on masked internal errors, and keep every other internal detail on the server (Class 6).

Schema outcomes

  1. Give outcome types machine-readable field paths so form UIs never parse message text (Class 9).
  2. Put a shared interface under typed outcome unions so old clients survive new outcomes (Class 5).
  3. Return the entity's true state alongside errors on failed updates (Class 10).
  4. Enforce "every union member implements the interface" with a linter or schema test, because nothing else will (Class 5).

Server

  1. Mask by default; an unrecognized exception is INTERNAL with an opaque message, logged in full (Class 6).
  2. Translate exceptions to classified errors at one point (resolver chain or advice) shared by all resolvers (Class 6).
  3. 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

  1. Support application/graphql-response+json and use the draft's status codes; 200 clean, 294 partial, specific 4xx unexecuted, 5xx server-fault (Class 3).

Client

  1. Receive partial data and errors together, and render both honestly at section granularity (Class 7).
  2. Centralize the reactions to error codes (re-auth, retry) in one link or interceptor (Class 7).
  3. Never let an error null be written to shared state as if it were a value (Class 7).

Direction

  1. Declare fields the domain always populates with @semanticNonNull, and track onError as 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:

  1. Codes. Start here, because everything downstream branches on them. Adopt a Class 4 vocabulary in extensions.code through 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.
  2. 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 4xx codes, and 294 is pending there (issue #1314), so a Spring server keeps sending 200 for partial results until that lands.
  3. 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.
  4. 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.
  5. Nullability. Annotate truly-never-absent fields with @semanticNonNull, which is additive and safe, per Class 8. Derive strict and nullable SDL views with graphql-sock where 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:

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 @semanticNonNull as 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

Design

Client and future

Case studies

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.