diff --git a/.prettierignore b/.prettierignore index 26cbea7e4e..07f4da3620 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,6 +7,7 @@ pnpm-lock.yaml !src/pages/blog/2025-05-31-graphiql-4/index.mdx !src/pages/blog/2025-06-10-graphiql-5/index.mdx !src/pages/blog/2025-06-19-multioption-inputs-with-oneof/index.mdx +!src/pages/blog/2026-08-14-true-nullability.mdx *.jpg scripts/**/*.json diff --git a/src/pages/blog/2026-08-14-true-nullability.mdx b/src/pages/blog/2026-08-14-true-nullability.mdx new file mode 100644 index 0000000000..6d30fe3c2e --- /dev/null +++ b/src/pages/blog/2026-08-14-true-nullability.mdx @@ -0,0 +1,429 @@ +--- +title: "Bringing 'True' Nullability to GraphQL" +tags: ["spec"] +date: 2026-08-14 +byline: Benjie Gillam +--- + +_Want to skip the history lesson and get to the good stuff? Jump to [Introducing +the `onError` request property](#introducing-the-onerror-request-property)._ + +One of GraphQL's early decisions was to allow "partial success"; this was a +critical feature for Facebook: if one part of their backend infrastructure +became degraded, they wouldn't want to just render an error page; instead, +they wanted to serve the user a page with as much working data as they could. + +## Null propagation + +To accomplish this, if an error occurred within a resolver, that field's +response position would be replaced with `null`, and an error would be added +to the `errors` array in the response. + +But what if that position was marked as non-null? + +To solve that apparent contradiction, GraphQL introduced "error propagation" +(aka "null bubbling"): when a `null` occurs in a non-nullable position, the +parent position is made `null` instead. If that position is also non-nullable, +its parent will be made `null` instead, and so on up the tree until a nullable +position is made `null`. + +This solved the issue and meant that GraphQL's nullability promises were still +honoured, but it wasn't without complications... + +### Complication 1: partial success + +We want to be resilient to systems failing, but errors that occur in +non-nullable positions cascade to surrounding parts of the query, making less +and less data available to be rendered. + +This seems contrary to our "partial success" aim, but it's easy to solve: we +just make sure that the positions where we expect errors to occur are nullable +so that errors don't propagate further. + +Unfortunately, this means clients now need `null`-handling code in a few more +places, but what is engineering if not choosing trade-offs? + +### Complication 2: nullable epidemic + +So... where are errors likely to occur? + +Almost any field in your GraphQL schema could raise an error. Errors might not +only be caused by backend services becoming unavailable or responding in +unexpected ways; they can also be caused by simple programming errors in your +business logic, data consistency errors (e.g., expecting a boolean but receiving +a float), access controls, or any other cause. + +Since we don't want to "blow up" the entire response if any such issue occurs, +we've come to strongly encourage nullable usage throughout a schema, only +adding the non-nullable `!` marker to positions where we're truly sure that the +field is extremely unlikely to error. + +This "nullable by default" approach means that developers consuming the GraphQL +API have to handle potential nulls in more positions than they would expect, +with an explosion of null checks leading people to even call into question the +value of GraphQL's "type safety". + +### Complication 3: normalised caching + +Many modern GraphQL clients use a "normalised" cache, such that updates pulled +down from the API in one query can automatically update all the previously +rendered data across the application. This helps ensure consistency for users +and is a powerful feature. + +However, if an error occurs in a non-nullable position, it's +[no longer safe](https://github.com/graphql/nullability-wg/issues/20) to store +the data in the normalised cache. + +## The Nullability Working Group + +At first, we thought the solution to these complications was to give clients +control over the nullability of each field in the response, so we set up the +Client-Controlled Nullability (CCN) Working Group. Later, we renamed the working +group to the Nullability WG to show that it encompassed all potential solutions +to this problem. + +### Client-controlled nullability + +The first Nullability WG proposal came from a collaboration between Yelp and +Netflix, with contributions from GraphQL WG regulars Alex Reilly, Mark Larah, +and Stephen Spalding, among others. They proposed that we could adorn the queries +we issue to the server with sigils indicating our desired nullability overrides +for the given fields: client-controlled nullability. + +A `?` would be added to fields where we don't mind if they're null but we +definitely want errors to stop there, and a `!` would be added to fields +where we definitely don't want a null to occur (whether or not there is an +error). This would give consumers control over where errors/nulls were handled. + +However, after much exploration of the topic over the years, we found +numerous issues that traded one set of concerns for another. We kept iterating +whilst we looked for a solution to these trade-offs. + +### True nullability schema + +Jordan Eldredge +[proposed](https://github.com/graphql/nullability-wg/discussions/22) that making +fields nullable to handle error propagation was hiding the "true" nullability of +the data. Instead, he suggested, we should have the schema represent the true +nullability, and put the responsibility on clients to use the `?` CCN operator +to handle errors in the relevant places. + +However, this would mean that clients such as Relay would want to add `?` in +every position, causing an "explosion" of question marks, because really what +Relay desired was to disable null propagation entirely. + +### A new type + +Getting the relevant experts together at GraphQLConf 2023 re-energised the +discussions and sparked new ideas. After seeing Stephen's "Nullability Sandwich" +talk and chatting with Jordan, Stephen, and others in the corridor, Benjie Gillam +was inspired to [propose](https://github.com/graphql/graphql-spec/pull/1046) a +"null only on error" type. This type would allow us to express the "true" +nullability of a field whilst also indicating that errors may occur and should +be handled without "blowing up" the response. + +To maintain backwards compatibility, clients would need to opt in to seeing this +new type (otherwise it would masquerade as nullable). It would be up to the +client to decide how to handle the nullability of this position, knowing that a +"null only on error" position would only contain a `null` if a matching error +existed in the `errors` list. + +A +[number of alternative syntaxes](https://gist.github.com/benjie/19d784721d1658b89fd8954e7ee07034) +were suggested for this new type, but none were well-liked. + +### A new approach to client error handling + +Also around the time of GraphQLConf 2023, the Relay team shared +[a presentation](https://docs.google.com/presentation/u/2/d/1rfWeBcyJkiNqyxPxUIKxgbExmfdjA70t/edit?pli=1#slide=id.p8) +on some of the things they were thinking around errors. In particular, they +discussed the `@catch` directive, which would give users control over how errors +were represented in the data being rendered, allowing the client to +differentiate an error from a legitimate null. Over the coming months, many +behaviours were discussed at the Nullability WG; one particularly compelling one +was that clients could throw the error when an errored field was read and rely +on framework mechanics (such as React's +[error boundaries](https://legacy.reactjs.org/docs/error-boundaries.html)) to +handle those errors. + +### Strict semantic nullability + +GraphQL Foundation director Lee Byron +[proposed](https://github.com/graphql/graphql-wg/discussions/1410) that we +introduce a schema directive, `@strictNullability`, whereby we would change what +the syntax meant: `Int?` for nullable, `Int` for null-only-on-error, and `Int!` +for never-null. This proposal was well-liked, but wasn't a clear win; it +introduced many complexities, including migration costs and concerns over schema +evolution. + +### A pivotal discussion + +Lee and Benjie had a call where they discussed the history of GraphQL +nullability and all the relevant proposals in depth, including their two +respective solutions. It was clear that, though no solution was quite there, +the solutions were converging, hinting that we were getting closer and closer to +an answer. + +This long, detailed, highly technical discussion ultimately led to the +realisation that error propagation itself was the issue. Rather than +working around error propagation with new "null only on error" types, what we +really needed was a way for "smart clients" to turn off error propagation +entirely. + +### `@experimental_disableErrorPropagation` + +Our first punt at this was the `@experimental_disableErrorPropagation` directive +that could be added to operations to disable error propagation. However, we +quickly realised that this was cumbersome and inconsistent, and that disabling +error propagation would also become the responsibility of the developer rather +than the client. A smart client that understands the schema should be able to +fully re-implement traditional error propagation locally: `data` and `errors` +contain all the information it would need to do so. And if a client supports +this, it would want to disable error propagation for every request... + +## Introducing the `onError` request property + +The new `onError` request property allows a client to indicate its preference +for how errors are handled by the GraphQL service (each option is detailed +below). For example: + +```diff + POST /graphql HTTP/1.1 + Host: example.com + Content-Type: application/json + Accept: application/graphql-response+json + + { + "query": "query UserProfile($id: ID!) { user(id: $id) { id name avatarUrl bestFriend { name } } }", ++ "onError": "NULL", + "variables": { "id": "27" } + } +``` + +Services that support `onError` must honour the specified behaviour. If a +service does not support `onError`, the request property will be ignored, +resulting in the traditional behaviour, equivalent to `onError: "PROPAGATE"`. + +Services will soon be able to indicate their support for the `onError` request +property through [service +capabilities](https://github.com/graphql/graphql-spec/pull/1208)), allowing +clients to auto-discover and depend upon this capability. + +### `onError: "PROPAGATE"` + +This is the traditional error propagation behaviour that we all know and... +"love"? + +Setting `onError: "PROPAGATE"` will be equivalent to the error behaviour in the +initial 2015 GraphQL Specification: error propagation/null bubbling. + +### `onError: "HALT"` + +Ad-hoc scripts and similar clients throw away entire responses if any error +occurs, but currently the server still computes the "partial success" response +anyway. This mode allows clients to indicate that if any error occurs, they +won't read the data: any error should result in a +`{ data: null, errors: [...] }` response, allowing the service to halt +execution when the first error occurs. + +### `onError: "NULL"` + +> **Clients take responsibility for interpreting the response as a whole... +> ensuring application code can never read an "error null"** + +This is what we're excited about! + +`onError: "NULL"` completely disables error propagation within the GraphQL +service. From an error perspective, every position in the response (fields and +lists alike) is an error boundary, as though it were nullable (for +error-handling only). This effectively changes the "non-nullable" type modifier +to mean "null only on error" or, equivalently, "not null unless an error +occurred". + +Clients that opt in to this behaviour take responsibility for interpreting the +response as a whole, correlating the `data` and `errors` properties of the +response. They must cross-check any `null` values against `errors` to ensure the +application can never read an "error null"[^1] as if it were a "semantic +null"[^2]. + +[^1]: A `null` that has an associated error in the "errors" list in the response. + +[^2]: A "true" `null` representing that the data itself doesn't exist: e.g., + Alice doesn't have a favourite band: `favoriteBand: null`.[^3] + +[^3]: About the "u"... Yes, we Brits write prose in English but code in American. + +### "Smart" clients + +`onError: "NULL"` is intended for use by "smart" clients such as Relay, Apollo +Client, URQL, and others that understand GraphQL deeply and are responsible for +the storage and retrieval of fetched GraphQL data. These clients are well +positioned to handle the responsibilities outlined above. + +By disabling error propagation, these clients will be able to safely update +their stores (including normalised stores) even when errors occur. Having fixed +that underlying issue, they can then expose the data to the application either +by reproducing the traditional error-propagation behavior locally (in which +application code will not know that anything has changed), or by giving the +application a more ergonomic error-handling infrastructure, by becoming an +"error-handling client". + +### "Error-handling clients" + +**An error-handling client is a client that ensures that an "error null" can never +be read by application code.** + +The simplest form of an error-handling client already exists: it's a client that +throws (or otherwise prevents reading `data`) when the `errors` property exists +in the response. Since `data` can never be read, no "error nulls" can be read. + +For serious applications, more robust error handling would be desired which +allows the application to make the most use of GraphQL's "partial success" aim. +So long as the client does not allow an "error null" to be read, it's an error +handling client. There's a number of techniques to achieve this, and you could +even use a combination: + +- throw on error: when an "error null" is to be accessed, throw an error + instead so that it can be handled with traditional exception handling + behaviors such as `raise/except`, `try/catch`, or ``. A + simple implementation of this can be found in + [graphql-toe](https://github.com/graphile/graphql-toe/blob/main/src/index.ts) + which can add this behavior to Apollo Client, URQL, graffle and even + `window.fetch()` in just 512 bytes of gzipped code. +- `@throwOnFieldError`: add this to a fragment in Relay, and when you attempt to + read the fragment if any of the fields errored then the fragment will throw +- result types: when reading a fragment, return an `Result` type so + if an error occured in the fragment you cannot read the data +- `@catch` to coerce errors into other values or behaviors +- maybe your new innovative idea? + +By disabling error propagation and performing these behaviors at the field or +fragment level, sibling fields no longer cause errors to impact unrelated areas +of the application. By exposing errors ergonomically, we can ensure it's easy +for developers to handle errors, rather than ignoring them and hoping for the +best. + +## True nullability + +For clients using `onError: "NULL"`, fields are either nullable or non-nullable, +just as with traditional error propagation. However, unlike with traditional +propagation, errors can be represented in any position: + +* nullable (e.g., `Int`): a value, an error, or a true `null`; +* non-nullable (e.g., `Int!`): a value, **or an error**. + +*(With traditional error propagation, non-nullable fields cannot represent an +error because the error propagates to the nearest nullable position. Not so with +`onError: "NULL"`!)* + +### Greenfield services + +If a GraphQL service can guarantee it will never need to perform error +propagation (for example, by requiring all clients to include `onError: "NULL"` +or `onError: "HALT"` in requests), then the schema can safely indicate to +clients the true intended nullability of a field in the traditional way: +with a `!`: + +```graphql +type User { + id: ID! + username: String! + organization: Organization! # Null only on error - a user definitely belongs + # to an organization, but the organizations + # service might be unavailable. + + mostRecentPost: Post # Deliberately nullable, since you may not have + # posted anything yet. + + posts: [Post!]! # No posts? Empty array. Array will never contain + # a semantic null. +} +``` + +### Services with legacy clients + +For GraphQL services that cannot guarantee that all clients will have error +propagation disabled, there's a little more work to do. + +Traditional clients still need their nullable error boundaries, but modern +clients that support `onError: "NULL"` would still treat these fields as truly +nullable, requiring null checks in application code that should never fire. + +We need a way of indicating fields that are "null only on error", whether you're +using traditional clients with error propagation or modern error-handling +clients. + +For this, we've standardised on the use of the transitional `@semanticNonNull` +directive until such time as all your clients can be error-handling clients: + +```graphql +type User { + id: ID! + username: String @semanticNonNull + organization: Organization @semanticNonNull + mostRecentPost: Post + posts: [Post] @semanticNonNull(levels: [0, 1]) +} +``` + +`@semanticNonNull` states that a field will only ever be `null` within the +`data` of the response if there is a matching error in the `errors` list. The +`levels` argument allows this directive to be applied to different list +positions. + +Here's how error-handling clients can interpret various combinations of +`@semanticNonNull`: + +| SDL | Interpretation | +| --------------------------------------------- | -------------- | +| `[[Int]]` | `[[Int]]` | +| `[[Int]] @semanticNonNull` | `[[Int]]!` | +| `[[Int]] @semanticNonNull(levels: [1])` | `[[Int]!]` | +| `[[Int]] @semanticNonNull(levels: [0, 1, 2])` | `[[Int!]!]!` | +| `[[Int]!] @semanticNonNull(levels: [2])` | `[[Int!]!]` | + +Of course, clients don't need to do this themselves; +[`graphql-sock`](https://www.npmjs.com/package/graphql-sock) (Semantic Output +Conversion Kit, a great pairing for `graphql-toe`) can be used to read an SDL +marked up with `@semanticNonNull` and output the equivalent SDL for either +error-handling clients (`semantic-to-strict`) or traditional clients +(`semantic-to-nullable`). + +## The future + +As clients and servers all adopt `onError: "NULL"`, traditional error +propagation should become a relic of the past. Application developers will not +need to look through the `errors` list in a response manually; instead, +error-handling clients will raise errors using ergonomic and familiar patterns +(for example, `Result<...>` types for fragment reads or simply throwing errors +when related data is read), fulfilling the promise of "partial success" that +came with GraphQL's launch all those years ago. + +Once all the clients a service serves are error-handling clients, schema +designers no longer need to factor "errorability" into their schema design. They +can indicate the true nullability of each field directly through the schema +with a `!`; clients will need fewer null checks, and the `@semanticNonNull` +directive can join error propagation as a relic of the past. + +Start integrating `onError: "NULL"` into your clients and services today, and +let's make this future of type safety and solid error handling a reality. + +## Help us get this merged! + +Whether you work on a GraphQL client library, server library, or framework, or +are just a GraphQL user with thoughts on nullability, we want to hear from you. +Have you tried `onError: "NULL"`, `@semanticNonNull`, `graphql-toe`, or [other +error handling +mechanisms](https://relay.dev/docs/guides/throw-on-field-error-directive/)? Like +all GraphQL Working Groups, the GraphQL Specification Working Group is open to +all: add yourself to an [upcoming working group +meeting](https://github.com/graphql/graphql-wg/) or chat with us in the +#nullability-wg channel in [the GraphQL Discord](https://discord.graphql.org). + +**The solution is fully formed and ready to go; we just need your adoption and +feedback to get it merged into the spec!** + +--- + +

clarity.inputfrom.ai