@@ -5,7 +5,7 @@ date: 2024-08-14
5
5
byline : Benjie Gillam
6
6
---
7
7
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
9
9
critical feature for Facebook - if one part of their backend infrastructure
10
10
became degraded they wouldn't want to just render an error page, instead they
11
11
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
18
18
solve that apparent contradiction, GraphQL introduced the "error propagation"
19
19
behavior (also known colloquially as "null bubbling") - when a ` null ` (from an
20
20
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.
23
25
24
26
This solved the issue, and meant that GraphQL's nullability promises were still
25
27
honoured; but it wasn't without complications.
26
28
27
- ### Complication 1: partial failures
29
+ ### Complication 1: partial success
28
30
29
31
We want to be resilient to systems failing; but errors that occur in
30
32
non-nullable positions cascade to surrounding parts of the query, making less
31
33
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
33
35
where we expect errors to occur are nullable so that errors don't propagate
34
36
further. Clients now needed to ensure they handle any nulls that occur in these
35
37
positions; but that seemed like a fair trade.
36
38
37
39
### Complication 2: nullable epidemic
38
40
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.
45
46
46
47
Since we don't want to "blow up" the entire response if any such issue occurred,
47
48
we've moved to strongly encourage nullable usage throughout a schema, only
48
49
adding the non-nullable ` ! ` marker to positions where we're truly sure that
49
50
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 .
52
53
53
54
### Complication 3: normalized caching
54
55
@@ -57,7 +58,7 @@ down from the API in one query can automatically update all the previously
57
58
rendered data across the application. This helps ensure consistency for users,
58
59
and is a powerful feature.
59
60
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
61
62
[ no longer safe] ( https://github.com/graphql/nullability-wg/issues/20 ) to store
62
63
the data to the normalized cache.
63
64
@@ -70,15 +71,20 @@ that it encompassed all potential solutions to this problem.
70
71
71
72
### Client-controlled nullability
72
73
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.
80
84
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.
82
88
83
89
### True nullability schema
84
90
@@ -96,24 +102,22 @@ Relay desired was to disable null propagation entirely.
96
102
### A new type
97
103
98
104
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"
105
109
nullability of a field whilst also indicating that errors may happen that should
106
110
be handled, but would not "blow up" the response.
107
111
108
112
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.
113
117
114
118
A
115
119
[ 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.
117
121
118
122
### A new approach to client error handling
119
123
@@ -129,30 +133,32 @@ on framework mechanics (such as React's
129
133
[ error boundaries] ( https://legacy.reactjs.org/docs/error-boundaries.html ) ) to
130
134
handle them.
131
135
132
- ### A new mode
136
+ ### Strict semantic nullability
133
137
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
135
140
introduce a schema directive, ` @strictNullability ` , whereby we would change what
136
141
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.
139
145
140
146
### A pivotal discussion
141
147
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
146
153
[ a new proposal] ( https://github.com/graphql/nullability-wg/discussions/58 ) ,
147
154
which has been iterated further, and we aim to describe below.
148
155
149
156
## Our latest proposal
150
157
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.
156
162
157
163
### No-error-propogation mode
158
164
@@ -161,10 +167,11 @@ The new proposal centers around the premise of allowing clients to disable the
161
167
162
168
Clients that opt-in to this behavior take responsibility for interpretting the
163
169
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.
168
175
169
176
### "Smart" clients
170
177
@@ -180,7 +187,7 @@ foundations, shielding applications developers from needing to learn this new
180
187
behavior (whilst still allowing them to reap the benefits!). They can even take
181
188
on advanced behaviors, such as throwing the error when the application developer
182
189
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.
184
191
185
192
### True nullability
186
193
@@ -190,28 +197,29 @@ mode, no-error-propagation mode allows for errors to be represented in any
190
197
position:
191
198
192
199
- 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** .
194
201
195
202
_ (In traditional mode, non-nullable fields cannot represent an error because the
196
203
error propagates to the nearest nullable position.)_
197
204
198
205
Since this mode allows every field, whether nullable or non-nullable, to
199
206
represent an error, the schema can safely indicate to clients in this mode the
200
207
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).
204
211
205
212
### Client reflection of true nullability
206
213
207
214
Smart clients can ask the schema about the "true" nullability of each field via
208
215
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.
215
223
216
224
### Schema developers
217
225
@@ -224,40 +232,40 @@ concern we've introduced the concept, of a "semantic" non-null type:
224
232
- "strict" (traditional) non-nullable - shows up as non-nullable in both
225
233
traditional mode and no-null-propagation mode
226
234
- "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
229
236
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.
233
240
234
241
### Representation in SDL
235
242
236
243
Application developers will only need to deal with traditional SDL that
237
244
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.
242
250
243
251
Client-derived SDL (see "client reflection of true nullability" above) can be
244
252
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!! ` |
261
269
262
270
The ` !! ` symbol is designed to look a little scary - it should be used with
263
271
caution (like ` ! ` in traditional schemas) because it is the symbol that means
0 commit comments