Skip to content

Commit 39f865f

Browse files
authored
docs: document @experimental_disableErrorPropagation (#4820)
1 parent 61db552 commit 39f865f

4 files changed

Lines changed: 298 additions & 1 deletion

File tree

website/pages/docs/_meta.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const meta = {
3535
'experimental-specification-features': '',
3636
'defer-stream': '',
3737
'fragment-arguments': '',
38-
'directives-on-directives': '',
38+
'disabling-error-propagation': '',
3939
'-- 4': {
4040
type: 'separator',
4141
title: 'GraphQL.js Runtime Features',
@@ -58,6 +58,7 @@ const meta = {
5858
'resolver-anatomy': '',
5959
'graphql-errors': '',
6060
'using-directives': '',
61+
'directives-on-directives': '',
6162
'authorization-strategies': '',
6263
'-- 6': {
6364
type: 'separator',
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
---
2+
title: Disabling Error Propagation
3+
sidebarTitle: Disabling Error Propagation
4+
---
5+
6+
# Disabling Error Propagation
7+
8+
GraphQL's traditional execution behavior propagates an execution error from a
9+
non-null field to the nearest nullable parent. This preserves the schema's
10+
strict non-null guarantee, but it can also destroy useful data from the response
11+
and make the result unsafe for normalized client caches.
12+
13+
GraphQL.js v17 exposes an experimental operation directive for services and
14+
clients that want to test the non-propagating error mode:
15+
16+
```graphql
17+
directive @experimental_disableErrorPropagation on QUERY | MUTATION | SUBSCRIPTION
18+
```
19+
20+
When an operation uses this directive, an execution error still appears in the
21+
`errors` list with its normal `path`, but the errored response position becomes
22+
`null` directly. The error no longer nulls out non-null parents.
23+
24+
## Motivation
25+
26+
The current
27+
[error behavior](https://github.com/graphql/graphql-spec/pull/1163) proposal
28+
describes error propagation as the source of two practical problems: it removes
29+
data that could otherwise be used, and it makes errored responses unsafe to
30+
write into normalized stores. The broader
31+
[Semantic Nullability RFC](https://github.com/graphql/graphql-wg/blob/main/rfcs/SemanticNullability.md)
32+
also traces the consequences through partial success, schema design, and
33+
normalized caching, and the Nullability WG captured the cache issue as
34+
[normalized-cache corruption from null bubbling](https://github.com/graphql/nullability-wg/issues/20).
35+
36+
The motivations are:
37+
38+
- Preserve useful partial data. A failed field should not necessarily remove
39+
sibling response data that resolved successfully.
40+
- Keep normalized caches safe. A bubbled ancestor `null` can look like a real
41+
object update when only one descendant field failed.
42+
- Separate execution errors from true semantic nullability. Schema nullability
43+
should be able to express whether `null` is a normal domain value, not whether
44+
a resolver might fail.
45+
46+
The core issue behind those motivations is that traditional error propagation
47+
couples two concerns:
48+
49+
- Whether `null` is a meaningful value for a field.
50+
- Whether resolving that field might fail.
51+
52+
Because any field can fail, schema authors often make fields nullable even when
53+
`null` is not a normal domain value. This avoids losing larger chunks of the
54+
response when a resolver fails, but it also creates a "nullable epidemic":
55+
clients and generated types must treat many semantically required values as
56+
nullable just because execution errors are possible.
57+
58+
Error propagation also creates a cache-safety problem for clients with normalized
59+
stores. If `viewer.name` is already cached and a later query for `viewer.age`
60+
raises an execution error, traditional propagation may replace the whole
61+
`viewer` object with `null`. Writing that bubbled `null` can corrupt the
62+
normalized cache entry for `viewer`, even though the already-known `name` value
63+
is still valid. With propagation disabled, the client can keep the `viewer`
64+
object, record the error at `["viewer", "age"]`, and avoid turning a field error
65+
into an ancestor-object cache update.
66+
67+
Disabling error propagation moves responsibility for execution-error handling to
68+
the client. Clients that read `errors` by path, throw on field errors, use error
69+
boundaries, or store errors alongside normalized data can keep the successful
70+
parts of the response without treating bubbled ancestor `null` values as real
71+
data.
72+
73+
This also opens the path toward semantic nullability, but with an important
74+
distinction: disabling propagation does not make every raw `null` in `data` a
75+
semantic `null`. A field that raised an execution error is still represented as
76+
`null`; the `errors` entry at the same path tells the client that this is an
77+
error `null`. What changes is that descendant errors no longer replace ancestor
78+
positions, so a `null` at a position without a matching execution error can more
79+
directly reflect that field's semantic nullability.
80+
81+
## Using it in GraphQL.js
82+
83+
Define the directive in the schema while this feature is experimental:
84+
85+
```graphql
86+
directive @experimental_disableErrorPropagation on QUERY | MUTATION | SUBSCRIPTION
87+
88+
type Query {
89+
viewer: User!
90+
}
91+
92+
type User {
93+
id: ID!
94+
displayName: String!
95+
nickname: String
96+
}
97+
```
98+
99+
Apply the directive to an operation:
100+
101+
```graphql
102+
query Profile @experimental_disableErrorPropagation {
103+
viewer {
104+
id
105+
displayName
106+
nickname
107+
}
108+
}
109+
```
110+
111+
If `displayName` throws, traditional propagation would null out `viewer`, and
112+
because `viewer` is non-null, the whole `data` entry could become `null`. With
113+
error propagation disabled, GraphQL.js reports the error at the field path and
114+
keeps the rest of the object:
115+
116+
```json
117+
{
118+
"data": {
119+
"viewer": {
120+
"id": "1",
121+
"displayName": null,
122+
"nickname": "Ada"
123+
}
124+
},
125+
"errors": [
126+
{
127+
"message": "Could not fetch display name.",
128+
"path": ["viewer", "displayName"]
129+
}
130+
]
131+
}
132+
```
133+
134+
The directive changes execution behavior only for operations that use it.
135+
Operations without the directive keep traditional GraphQL error propagation.
136+
137+
This directive only tests one behavior: error propagation is disabled for the
138+
operation. It is not the final shape of the broader standards proposal.
139+
140+
## Relationship to `onError`
141+
142+
The newer draft
143+
[`onError` GraphQL.js implementation](https://github.com/graphql/graphql-js/pull/4364)
144+
tracks the
145+
[error behavior](https://github.com/graphql/graphql-spec/pull/1163) spec
146+
proposal, which generalizes this idea into a client-selected `onError` behavior.
147+
Rather than only toggling propagation on or off, the proposal describes multiple
148+
modes, including:
149+
150+
- Traditional propagation behavior.
151+
- Resolving errored positions to `null` without propagating to non-null parents.
152+
- Halting execution on the first execution error.
153+
154+
The current `@experimental_disableErrorPropagation` directive lets GraphQL.js
155+
users experiment with the second mode. Clients can use it to test error handling
156+
by path, normalized-cache behavior, and UI error boundaries before the request
157+
shape is standardized.
158+
159+
The `onError` proposal relies on
160+
[Service capabilities](https://github.com/graphql/graphql-spec/pull/1208)
161+
because clients need a way to discover support and defaults before choosing a
162+
mode automatically. A client cannot safely assume that a service understands a
163+
new request parameter, supports every proposed mode, or uses the same default
164+
error behavior. Capabilities give tools and clients an in-band way to learn what
165+
the service supports and how to configure themselves without out-of-band
166+
instructions.
167+
168+
## Service capabilities and introspection
169+
170+
Service capabilities are proposed as an introspection feature. They were
171+
originally part of the error-behavior work and were later extracted into their
172+
own [Service capabilities](https://github.com/graphql/graphql-spec/pull/1208)
173+
proposal. Existing GraphQL introspection describes the schema's type system:
174+
object types, fields, arguments, directives, and related schema metadata. Service
175+
capabilities extend that idea to service-level behavior that is outside the type
176+
system, such as which experimental syntax, transport features, or error
177+
behaviors the service supports.
178+
179+
The proposal introduces a service introspection entry point, currently described
180+
as a `__service` meta-field, that returns capability records. For error
181+
behavior, those records let a client discover whether the service supports
182+
client-selected `onError` behavior, which modes are accepted, and what default
183+
mode applies when the request does not choose one.
184+
185+
That distinction matters for clients. A schema can contain the same fields and
186+
types whether it uses traditional error propagation or a non-propagating error
187+
mode, so ordinary type introspection is not enough to configure the client
188+
safely. Capabilities make that operational contract introspectable.
189+
190+
## Proposal history
191+
192+
This area has gone through several proposals. The broader
193+
[Semantic Nullability RFC](https://github.com/graphql/graphql-wg/blob/main/rfcs/SemanticNullability.md)
194+
collects the problem history and solution matrix.
195+
196+
The latest proposals to watch are:
197+
198+
- [Error behavior](https://github.com/graphql/graphql-spec/pull/1163) defines
199+
the client-selected `onError` modes. GraphQL.js v17 makes one part of this
200+
current proposal available experimentally through
201+
`@experimental_disableErrorPropagation`: the mode where errored positions
202+
resolve to `null` without propagating through non-null parents.
203+
- [Service capabilities](https://github.com/graphql/graphql-spec/pull/1208)
204+
is the capabilities work extracted from the error-behavior proposal. It
205+
defines the introspection mechanism for advertising service-level features and
206+
configuration. It is related to error behavior because clients need to
207+
discover whether `onError` is supported, which modes may be requested, and
208+
which behavior applies when no mode is requested.
209+
210+
## What is next
211+
212+
The current GraphQL.js directive is intentionally experimental and uses an
213+
implementation-prefixed name. Future standards work is expected to settle the
214+
request shape and discovery story separately:
215+
216+
- The error-behavior proposal defines the execution modes.
217+
- Semantic nullability work defines how schemas and clients distinguish values
218+
that are nullable in normal business logic from values that are only `null`
219+
because an execution error occurred.
220+
- The service-capabilities proposal defines how clients discover whether those
221+
modes are supported and what the default behavior is.
222+
223+
Until that settles, clients should only use
224+
`@experimental_disableErrorPropagation` with services that document support for
225+
it. When it is enabled, clients should treat `null` at an errored path as an
226+
error `null`, not as a semantic `null`.

website/pages/docs/experimental-specification-features.mdx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,33 @@ value is `specifiedDirectives`.
3131

3232
See [Defer and Stream](/docs/defer-stream).
3333

34+
## Disabling error propagation
35+
36+
Disabling error propagation lets an execution error resolve the errored response
37+
position to `null` without nulling out non-null parent fields. GraphQL.js exposes
38+
this through the experimental operation directive
39+
`@experimental_disableErrorPropagation`.
40+
41+
The motivations are:
42+
43+
- Preserve useful partial data. A failed field should not necessarily remove
44+
sibling response data that resolved successfully.
45+
- Keep normalized caches safe. A bubbled ancestor `null` can look like a real
46+
object update when only one descendant field failed.
47+
- Separate execution errors from true semantic nullability. Schema nullability
48+
should be able to express whether `null` is a normal domain value, not whether
49+
a resolver might fail.
50+
51+
This feature is part of a longer nullability and error-handling proposal thread.
52+
The latest proposals to follow are
53+
[Error Behavior](https://github.com/graphql/graphql-spec/pull/1163), which
54+
defines client-selected `onError` modes, and
55+
[Service Capabilities](https://github.com/graphql/graphql-spec/pull/1208), which
56+
pulls the capabilities work out into the introspection mechanism clients use to
57+
discover support, accepted modes, and default behavior.
58+
59+
See [Disabling Error Propagation](/docs/disabling-error-propagation).
60+
3461
## Fragment arguments
3562

3663
Fragment arguments let named fragments declare local variables and let named

website/pages/upgrade-guides/v16-v17.mdx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,45 @@ with `singleResult` as a discriminator is gone. Remove branches that check for
371371
Schema setup, directive validation, result shapes, and transport guidance are
372372
covered in [Defer and Stream](/docs/defer-stream).
373373

374+
### Disabling error propagation
375+
376+
**Experimental or opt-in.** GraphQL.js v17 includes experimental support for
377+
operations that disable non-null error propagation. Add the directive definition
378+
to schemas that support it, then apply the directive to an operation:
379+
380+
```graphql
381+
directive @experimental_disableErrorPropagation on QUERY | MUTATION | SUBSCRIPTION
382+
383+
query Profile @experimental_disableErrorPropagation {
384+
viewer {
385+
name
386+
}
387+
}
388+
```
389+
390+
When a selected non-null field raises an execution error, GraphQL.js records the
391+
error at its path and resolves that response position to `null` directly instead
392+
of nulling out non-null parent fields.
393+
394+
The motivations are:
395+
396+
- Preserve useful partial data. A failed field should not necessarily remove
397+
sibling response data that resolved successfully.
398+
- Keep normalized caches safe. A bubbled ancestor `null` can look like a real
399+
object update when only one descendant field failed.
400+
- Separate execution errors from true semantic nullability. Schema nullability
401+
should be able to express whether `null` is a normal domain value, not whether
402+
a resolver might fail.
403+
404+
This directive only disables propagation. The newer draft
405+
[`onError` GraphQL.js implementation](https://github.com/graphql/graphql-js/pull/4364)
406+
tracks a proposal for multiple client-selected error behavior modes, such as
407+
traditional propagation, resolving errored positions to `null`, or halting on the
408+
first execution error. The GraphQL.js directive is useful for testing the
409+
non-propagating mode before the request shape is standardized.
410+
411+
See [Disabling Error Propagation](/docs/disabling-error-propagation).
412+
374413
**Behavioral tightening.** Defer/stream validation now tracks named fragment
375414
spreads through the selected operation rather than treating every fragment use
376415
in a document the same way. Subscription operations reject active `@defer` and
@@ -981,6 +1020,10 @@ These v16 APIs still work in v17, but are deprecated for removal in v18:
9811020
`graphql()` parse, validate, execute, or subscribe phases.
9821021
- Enable `experimentalFragmentArguments` only for hosts that intentionally
9831022
support arguments on named fragment spreads.
1023+
- Enable `@experimental_disableErrorPropagation` to test the non-propagating
1024+
error mode when clients intentionally handle field errors by path; track
1025+
[Service capabilities](https://github.com/graphql/graphql-spec/pull/1208) for
1026+
discovery of future `onError` support, supported modes, and defaults.
9841027

9851028
## Deep Import Moves
9861029

0 commit comments

Comments
 (0)