Skip to content

Commit 0965218

Browse files
committed
Some edits
1 parent f6fef9f commit 0965218

File tree

1 file changed

+94
-86
lines changed

1 file changed

+94
-86
lines changed

src/pages/blog/2024-08-14-exploring-true-nullability.mdx

Lines changed: 94 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ date: 2024-08-14
55
byline: Benjie Gillam
66
---
77

8-
One of GraphQL's early decisions was to handle "partial failures"; this was a
8+
One of GraphQL's early decisions was to allow "partial success"; this was a
99
critical feature for Facebook - if one part of their backend infrastructure
1010
became degraded they wouldn't want to just render an error page, instead they
1111
wanted to serve the user a page with as much working data as they could.
@@ -18,37 +18,38 @@ array in the response. However, what if that field was marked as non-null? To
1818
solve that apparent contradiction, GraphQL introduced the "error propagation"
1919
behavior (also known colloquially as "null bubbling") - when a `null` (from an
2020
error or otherwise) occurs in a non-nullable position, the parent position
21-
(either a field or a list item) is made `null` and this behavior would repeat if
22-
the parent position was also non-nullable.
21+
(either a field or a list item) is made `null` instead. This behavior would
22+
repeat if the parent position was also non-nullable, and this could cascade (or
23+
"bubble") all the way up to the root of the query if everything in the path is
24+
non-nullable.
2325

2426
This solved the issue, and meant that GraphQL's nullability promises were still
2527
honoured; but it wasn't without complications.
2628

27-
### Complication 1: partial failures
29+
### Complication 1: partial success
2830

2931
We want to be resilient to systems failing; but errors that occur in
3032
non-nullable positions cascade to surrounding parts of the query, making less
3133
and less data available to be rendered. This seems contrary to our "partial
32-
failures" aim, but it's easy to solve - we just make sure that the positions
34+
success" aim, but it's easy to solve - we just make sure that the positions
3335
where we expect errors to occur are nullable so that errors don't propagate
3436
further. Clients now needed to ensure they handle any nulls that occur in these
3537
positions; but that seemed like a fair trade.
3638

3739
### Complication 2: nullable epidemic
3840

39-
But, it turns out, almost any field in your GraphQL schema could raise an error
40-
41-
- errors might not only be caused by backend services becoming unavailable or
42-
responding in unexpected ways; they can also be caused by simple programming
43-
errors in your business logic, data consistency errors (e.g. expecting a
44-
boolean but receiving a float), or any other cause.
41+
Almost any field in your GraphQL schema could raise an error - errors might not
42+
only be caused by backend services becoming unavailable or responding in
43+
unexpected ways; they can also be caused by simple programming errors in your
44+
business logic, data consistency errors (e.g. expecting a boolean but receiving
45+
a float), or any other cause.
4546

4647
Since we don't want to "blow up" the entire response if any such issue occurred,
4748
we've moved to strongly encourage nullable usage throughout a schema, only
4849
adding the non-nullable `!` marker to positions where we're truly sure that
4950
field is extremely unlikely to error. This has the effect of meaning that
50-
developers consuming the GraphQL API have to handle null in more positions than
51-
they would expect, giving them a harder time.
51+
developers consuming the GraphQL API have to handle potential nulls in more
52+
positions than they would expect, making for additional work.
5253

5354
### Complication 3: normalized caching
5455

@@ -57,7 +58,7 @@ down from the API in one query can automatically update all the previously
5758
rendered data across the application. This helps ensure consistency for users,
5859
and is a powerful feature.
5960

60-
But if an error occurs in a non-nullable position, it's
61+
However, if an error occurs in a non-nullable position, it's
6162
[no longer safe](https://github.com/graphql/nullability-wg/issues/20) to store
6263
the data to the normalized cache.
6364

@@ -70,15 +71,20 @@ that it encompassed all potential solutions to this problem.
7071

7172
### Client-controlled nullability
7273

73-
The first CCN WG proposal was that we could adorn the queries we issue to the
74-
server with sigils indicating our desired nullability overrides for the given
75-
fields - a `?` would be added to fields where we don't mind if they're null, but
76-
we definitely want errors to stop there; and add a `!` to fields where we
77-
definitely don't want a null to occur. This would give consumers control over
78-
where errors/nulls were handled; but after much exploration of the topic over
79-
years we found numerous issues that traded one set of concerns for another.
74+
The first Nullability WG proposal came from a collaboration between Yelp and
75+
Netflix, with contributions from GraphQL WG regulars Alex Reilly, Mark Larah,
76+
and Stephen Spalding among others. They proposed we could adorn the queries we
77+
issue to the server with sigils indicating our desired nullability overrides for
78+
the given fields - client-controlled nullability.
79+
80+
A `?` would be added to fields where we don't mind if they're null, but we
81+
definitely want errors to stop there; and add a `!` to fields where we
82+
definitely don't want a null to occur (whether or not there is an error). This
83+
would give consumers control over where errors/nulls were handled.
8084

81-
We needed a better solution.
85+
However, after much exploration of the topic over years we found numerous issues
86+
that traded one set of concerns for another. We kept iterating whilst we looked
87+
for a solution to these tradeoffs.
8288

8389
### True nullability schema
8490

@@ -96,24 +102,22 @@ Relay desired was to disable null propagation entirely.
96102
### A new type
97103

98104
Getting the relevant experts together at GraphQLConf 2023 re-energized the
99-
discussions and sparked new ideas. After seeing Stephen Spalding's "Nullability
100-
Sandwich" talk and chatting with Jordan, Stephen and others in amongst the
101-
seating, Benjie had an idea that felt right to him. He grabbed his laptop and
102-
sat quietly for an hour at one of the tables in the sponsors room and wrote up
103-
[the spec edits](https://github.com/graphql/graphql-spec/pull/1046) to represent
104-
a "null only on error" type. This type would allow us to express the "true"
105+
discussions and sparked new ideas. After seeing Stephen's "Nullability Sandwich"
106+
talk and chatting with Jordan, Stephen and others in the corridor, Benjie Gillam
107+
was inspired to [propose](https://github.com/graphql/graphql-spec/pull/1046) a
108+
"null only on error" type. This type would allow us to express the "true"
105109
nullability of a field whilst also indicating that errors may happen that should
106110
be handled, but would not "blow up" the response.
107111

108112
To maintain backwards compatibility, clients would need to opt in to seeing this
109-
new type (otherwise it would masquerade as nullable); and it would be their
110-
choice of how to handle the nullability of this position, knowing that the data
111-
would only contain a `null` there if a matching error existed in the `errors`
112-
list.
113+
new type (otherwise it would masquerade as nullable). It would be up to the
114+
client how to handle the nullability of this position knowing that a "null only
115+
on error" position would only contain a `null` if a matching error existed in
116+
the `errors` list.
113117

114118
A
115119
[number of alternative syntaxes](https://gist.github.com/benjie/19d784721d1658b89fd8954e7ee07034)
116-
were suggested for this, but none were well liked.
120+
were suggested for this new type, but none were well liked.
117121

118122
### A new approach to client error handling
119123

@@ -129,30 +133,32 @@ on framework mechanics (such as React's
129133
[error boundaries](https://legacy.reactjs.org/docs/error-boundaries.html)) to
130134
handle them.
131135

132-
### A new mode
136+
### Strict semantic nullability
133137

134-
Lee [proposed](https://github.com/graphql/graphql-wg/discussions/1410) that we
138+
GraphQL Foundation director Lee Byron
139+
[proposed](https://github.com/graphql/graphql-wg/discussions/1410) that we
135140
introduce a schema directive, `@strictNullability`, whereby we would change what
136141
the syntax meant - `Int?` for nullable, `Int` for null-only-on-error, and `Int!`
137-
for never-null. This proposal was well liked, but wasn't a clear win, it
138-
introduced many complexities, not least migration costs.
142+
for never-null. This proposal was well liked, but wasn't a clear win; it
143+
introduced many complexities including migration costs and concerns over schema
144+
evolution.
139145

140146
### A pivotal discussion
141147

142-
Lee and Benjie had a call where they discussed all of this in depth, including
143-
their two respective solutions, their pros and cons. It was clear that neither
144-
solution was quite there, but we were getting closer and closer to a solution.
145-
This long and detailed highly technical discussion inspired Benjie to write up
148+
Lee and Benjie had a call where they discussed the history of GraphQL
149+
nullability and all the relevant proposals in depth, including their two
150+
respective solutions. It was clear that though no solution was quite there, the
151+
solutions converging hinted we were getting closer and closer to an answer. This
152+
long and detailed highly technical discussion inspired
146153
[a new proposal](https://github.com/graphql/nullability-wg/discussions/58),
147154
which has been iterated further, and we aim to describe below.
148155

149156
## Our latest proposal
150157

151-
We're now proposing a new opt-in mode to solve the nullability problem. It's
152-
important to note that clients and servers that don't opt-in will be completely
153-
unaffected by this change (and a client may opt-in without a server opting-in,
154-
and vice-versa, without causing any issues - in these cases, traditional mode
155-
will be used).
158+
We're now proposing a new opt-in execution mode to solve the nullability
159+
problem. It's important to note that both the client and the server must opt-in
160+
to this new mode for it to take effect, otherwise the traditional execution mode
161+
will be used.
156162

157163
### No-error-propogation mode
158164

@@ -161,10 +167,11 @@ The new proposal centers around the premise of allowing clients to disable the
161167

162168
Clients that opt-in to this behavior take responsibility for interpretting the
163169
response as a whole, correlating the `data` and `errors` properties of the
164-
response. With error propagation disabled and the fact that any field could
165-
potentially throw an error, all positions in `data` can potentially contain a
166-
`null` value. Clients in this mode must cross-check any `null` values against
167-
`errors` to determine if it's a true null, or an error.
170+
response. With error propagation disabled and the previously discussed fact that
171+
any field could potentially throw an error, all positions in `data` can
172+
potentially contain a `null` value. Clients in this mode must cross-check any
173+
`null` values against `errors` to determine if it represents a true `null`, or
174+
an error.
168175

169176
### "Smart" clients
170177

@@ -180,7 +187,7 @@ foundations, shielding applications developers from needing to learn this new
180187
behavior (whilst still allowing them to reap the benefits!). They can even take
181188
on advanced behaviors, such as throwing the error when the application developer
182189
attempts to read from an errored field, allowing the developer to handle errors
183-
with their own more natural error boundaries.
190+
with their system's native error boundaries.
184191

185192
### True nullability
186193

@@ -190,28 +197,29 @@ mode, no-error-propagation mode allows for errors to be represented in any
190197
position:
191198

192199
- nullable (e.g. `Int`): a value, an error, or a true `null`;
193-
- non-nullable (e.g. `Int!`): a value **or an error**.
200+
- non-nullable (e.g. `Int!`): a value, **or an error**.
194201

195202
_(In traditional mode, non-nullable fields cannot represent an error because the
196203
error propagates to the nearest nullable position.)_
197204

198205
Since this mode allows every field, whether nullable or non-nullable, to
199206
represent an error, the schema can safely indicate to clients in this mode the
200207
true intended nullability of a field. If the schema designer knows that a field
201-
should never be null unless an error occurs, they would mark the field as
202-
non-nullable (but only for clients in no-null-propagation mode; see "schema
203-
developers" below).
208+
should never be null unless an error occurs, they can mark the field as
209+
"non-nullable for clients in no-error-propagation mode" (see "schema developers"
210+
below).
204211

205212
### Client reflection of true nullability
206213

207214
Smart clients can ask the schema about the "true" nullability of each field via
208215
introspection, and can generate a derived SDL by combining that information with
209-
their knowledge of how the client handles errors. This derived SDL would look
210-
like the traditional representation of the schema, but with more fields
211-
represented as non-nullable where the true nullability of the underlying schema
212-
is reflected. Application developers would issue queries and mutations in the
213-
same way they always had, but now their generated types don't need to handle
214-
`null` in as many positions as before, increasing developer happiness.
216+
their knowledge of how the client handles errors. This derived SDL, dependent on
217+
client behavior, would look like the traditional representation of the schema,
218+
but with more fields potentially marked as non-nullable where the true
219+
nullability of the underlying schema has been reflected. Application developers
220+
would issue queries and mutations in the same way they always had, but now their
221+
generated types may not need to handle `null` in as many positions as before,
222+
increasing developer happiness.
215223

216224
### Schema developers
217225

@@ -224,40 +232,40 @@ concern we've introduced the concept, of a "semantic" non-null type:
224232
- "strict" (traditional) non-nullable - shows up as non-nullable in both
225233
traditional mode and no-null-propagation mode
226234
- "semantic" non-nullable, aka "null only on error" - shows up as non-nullable
227-
only in no-null-propagation mode; in traditional mode it will masquerade as
228-
nullable
235+
in no-null-propagation mode and masquerades as nullable in traditional mode
229236

230-
Only clients that opt-in to seeing the true nullability will see this
231-
difference, otherwise the nullability of the chosen mode (traditional or
232-
no-error-propagation) will be reflected by introspection.
237+
Only clients that opt-in to seeing the "true" nullability will see these two
238+
different types of nullability, otherwise the nullability of the chosen mode
239+
(traditional or no-error-propagation) will be reflected by introspection.
233240

234241
### Representation in SDL
235242

236243
Application developers will only need to deal with traditional SDL that
237244
represents traditional nullability concerns. If these developers are using
238-
"smart" clients then they should get this SDL from the client rather than from
239-
the server, this allows them to see the nullability that the client guarantees
240-
based on how it will handle the "true" nullability of the schema, how it handles
241-
errors, and factoring in any local schema extensions that may have been added.
245+
"smart" clients then they should source this SDL from the client rather than
246+
from the server, this allows them to see the nullability that the client
247+
guarantees based on how it will handle the "true" nullability of the schema, how
248+
it handles errors, and factoring in any local schema extensions that may have
249+
been added.
242250

243251
Client-derived SDL (see "client reflection of true nullability" above) can be
244252
used for concerns such as code generation, which will work in the traditional
245-
way with no need for changes (but happier developers since there will be fewer
246-
nullable positions!).
247-
248-
However, schema developers and people working on "smart" clients may need to
249-
represent the differences between "strict" and "semantic" non-nullable in SDL.
250-
For these people, we're introducing the `@extendedNullability` document
251-
directive. When this directive is present at the top of a document, the `!`
252-
symbol means that a type will appear as non-nullable only in no-null-propagation
253-
mode, and a new `!!` symbol will represent that a type will appear as
254-
non-nullable in both traditional and no-error-propagation mode.
255-
256-
| Traditional Mode | No-null-propagation mode | Example |
257-
| ---------------- | ------------------------ | ------- |
258-
| Nullable | Nullable | `Int` |
259-
| Nullable | Non-nullable | `Int!` |
260-
| Non-nullable\* | Non-nullable | `Int!!` |
253+
way with no need for changes (but happier developers if there are fewer nullable
254+
positions!).
255+
256+
Schema developers and people working on "smart" clients may need to represent
257+
the differences between "strict" and "semantic" non-nullable in SDL. For these
258+
people, we're introducing the `@extendedNullability` document directive. When
259+
this directive is present at the top of a document, the `!` symbol means that a
260+
type will appear as non-nullable only in no-error-propagation mode, and a new
261+
`!!` symbol will represent that a type will appear as non-nullable in both
262+
traditional and no-error-propagation mode.
263+
264+
| Traditional Mode | No-error-propagation mode | Example |
265+
| ---------------- | ------------------------- | ------- |
266+
| Nullable | Nullable | `Int` |
267+
| Nullable | Non-nullable | `Int!` |
268+
| Non-nullable\* | Non-nullable | `Int!!` |
261269

262270
The `!!` symbol is designed to look a little scary - it should be used with
263271
caution (like `!` in traditional schemas) because it is the symbol that means

0 commit comments

Comments
 (0)