Skip to content

Commit 7032c7f

Browse files
authored
Disable Null Propagation Proposal (#1694)
1 parent 6d02e1f commit 7032c7f

File tree

1 file changed

+238
-0
lines changed

1 file changed

+238
-0
lines changed
+238
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
# RFC: Disable Error Propagation Directive
2+
3+
**Proposed by:** [Martin Bonnin](https://github.com/martinbonnin)
4+
5+
**See also:** [Original proposal/spec edits by Benjie](https://github.com/graphql/graphql-spec/pull/1050)
6+
7+
**Implementation PR**: https://github.com/graphql/graphql-js/pull/4348
8+
9+
This RFC proposes adding a new directive `@disableErrorPropagation` that allows clients to disable error propagation for specific operations in their GraphQL queries.
10+
11+
## 📜 Problem Statement
12+
13+
In GraphQL, nullability serves two distinct purposes:
14+
15+
1. **Semantic null**: Indicating that a field can have a legitimate "null" value (e.g., a user without an avatar)
16+
2. **Error handling**: Allowing errors to propagate up through nullable parent fields
17+
18+
This coupling of nullability and errors makes it difficult for clients to distinguish between semantic nulls and error states by looking at the schema. When a field resolver throws an error and the field is non-nullable, the error propagates up through parent fields until it reaches a nullable field, potentially nullifying a large portion of the response.
19+
20+
While this behavior helps maintain data consistency guarantees, there are cases where clients may want more granular control over error propagation, particularly when partial data is preferable to no data.
21+
22+
### Current Behavior
23+
24+
Consider this schema:
25+
26+
```graphql
27+
type User {
28+
id: ID!
29+
name: String
30+
posts: [Post!]!
31+
optionalPosts: [Post!]
32+
}
33+
34+
type Post {
35+
id: ID!
36+
title: String!
37+
content: String
38+
}
39+
```
40+
41+
If a resolver for `Post.title` throws an error:
42+
1. The error is added to the `errors` array in the response
43+
2. Since `title` is non-nullable (`String!`), its parent `Post` object must be null
44+
3. Since the array items are non-nullable (`[Post!]`), the entire `posts` array must be null
45+
4. Since `posts` is non-nullable (`[Post!]!`), its parent `User` object must be null
46+
5. This continues up the response tree until reaching a nullable field
47+
48+
For the `optionalPosts` field, which is nullable (`[Post!]`), the propagation would stop at that field, setting it to null.
49+
50+
This behavior ensures type safety but can lead to situations where a single error in a deeply nested non-nullable field causes a large portion of the response to be nullified, even when the remaining data might still be useful to the client.
51+
52+
### Use Cases
53+
54+
1. **Normalized Cache Protection**: When clients like Relay maintain a normalized cache, error propagation can cause cache corruption. For example, if two different queries fetch the same entity but one query errors on a non-nullable field, the error propagation can cause valid data from the other query to be nullified in the cache. Disabling error propagation allows clients to preserve valid data in their normalized caches while still handling errors appropriately.
55+
56+
2. **Partial Data Acceptance**: In some applications, receiving partial data with errors is preferable to receiving null. For example, in a feed-style application, if one post fails to load, the client might still want to display the other posts.
57+
58+
3. **Fine-grained Error Control**: Clients may want to specify different error handling behaviors for different parts of their queries based on their application requirements.
59+
60+
## ✅ RFC Goals
61+
62+
- Provide a way for clients to disable error propagation for specific operations
63+
- Help uncouple nullability and error handling in GraphQL
64+
- Support the transition to more semantically accurate nullability in schemas
65+
- Maintain backward compatibility with existing GraphQL behavior
66+
- Keep the implementation simple and focused
67+
68+
## 🚫 RFC Non-goals
69+
70+
- This is not intended to be a general-purpose error handling solution
71+
72+
## 🧑‍💻 Proposed Solution
73+
74+
Add a new directive `@disableErrorPropagation` that can be applied to operations in executable documents:
75+
76+
```graphql
77+
directive @disableErrorPropagation on QUERY | MUTATION | SUBSCRIPTION
78+
79+
query GetUserPosts @disableErrorPropagation {
80+
user {
81+
id
82+
name
83+
posts {
84+
id
85+
title
86+
content
87+
}
88+
}
89+
}
90+
```
91+
92+
When this directive is present on an operation:
93+
1. Errors thrown during execution will still be added to the `errors` array
94+
2. The errors will not cause nullability violations to propagate up through parent fields
95+
96+
### Example
97+
98+
Given these types:
99+
100+
```graphql
101+
type User {
102+
id: ID!
103+
name: String
104+
posts: [Post!]!
105+
}
106+
107+
type Post {
108+
id: ID!
109+
title: String!
110+
content: String
111+
}
112+
```
113+
114+
And this query:
115+
116+
```graphql
117+
query GetUserPosts @disableErrorPropagation {
118+
user {
119+
id
120+
name
121+
posts {
122+
id
123+
title
124+
content
125+
}
126+
}
127+
}
128+
```
129+
130+
If the `title` resolver for one of the posts throws an error:
131+
132+
**Current behavior** (without directive):
133+
```json
134+
{
135+
"data": null,
136+
"errors": [
137+
{
138+
"message": "Failed to load title",
139+
"path": ["user", "posts", 0, "title"]
140+
}
141+
]
142+
}
143+
```
144+
145+
**With @disableErrorPropagation**:
146+
```json
147+
{
148+
"data": {
149+
"user": {
150+
"id": "123",
151+
"name": "Alice",
152+
"posts": [
153+
{
154+
"id": "post1",
155+
"title": null,
156+
"content": "Some content"
157+
}
158+
]
159+
}
160+
},
161+
"errors": [
162+
{
163+
"message": "Failed to load title",
164+
"path": ["user", "posts", 0, "title"]
165+
}
166+
]
167+
}
168+
```
169+
170+
## 🛠️ Implementation Considerations
171+
172+
The implementation requires changes to the execution algorithm in graphql-js:
173+
174+
1. During operation execution, check if the operation has the `@disableErrorPropagation` directive
175+
2. If present, modify error handling to prevent propagation while still collecting errors
176+
3. Return field values as-is, even if errors occurred
177+
178+
While this proposal focuses on a simple boolean directive, future extensions might consider additional error behaviors:
179+
180+
- `PROPAGATE`: Current default behavior (errors propagate up)
181+
- `NULL`: Replace errored positions with null
182+
- `ABORT`: Abort the entire request on any error
183+
184+
These additional behaviors are not part of this proposal but may be considered in future iterations.
185+
186+
## 🗺️ Migration Path
187+
188+
For client authors wishing to adopt this feature:
189+
190+
1. Ensure your GraphQL server implementation supports `@disableErrorPropagation`. Clients can check for directive support by looking for the existence of the directive on introspection.
191+
2. Update client code to handle both nulls and errors appropriately.
192+
3. Add the directive to operations where you want to prevent error propagation. Many clients, especially those with normalized caches, will wish to apply `@disableErrorPropagation` to all operations.
193+
194+
For schema authors wishing to adopt this feature:
195+
196+
1. Update servers to a version that has support for `@disableErrorPropagation`
197+
198+
Once upgraded, schema authors may feel more comfortable using non-nullable fields. We'll want to keep an eye on that and how it affects developers using older or simpler clients.
199+
200+
## 🤔 Alternatives Considered
201+
202+
### Configurable Error Behavior
203+
204+
Instead of a simple boolean directive, we could provide an enum of error behaviors:
205+
206+
```graphql
207+
enum ErrorBehavior {
208+
"""
209+
Non-nullable positions that error cause the error to propagate to the nearest nullable
210+
ancestor position. The error is added to the "errors" list.
211+
"""
212+
PROPAGATE
213+
214+
"""
215+
Positions that error are replaced with a `null` and an error is added to the "errors"
216+
list.
217+
"""
218+
NULL
219+
220+
"""
221+
If any error occurs, abort the entire request and just return the error in the "errors"
222+
list. (No partial success.)
223+
"""
224+
ABORT
225+
}
226+
227+
directive @errorBehavior(behavior: ErrorBehavior!) on QUERY | MUTATION | SUBSCRIPTION
228+
```
229+
230+
The simple boolean `@disableErrorPropagation` directive was chosen as it:
231+
- Addresses the most common use case (wanting partial data)
232+
- Maintains GraphQL's existing error representation
233+
- Is simple to implement and understand
234+
- Leaves room for future extensions if more sophisticated error handling patterns prove necessary
235+
236+
## 📝 Conclusion
237+
238+
The `@disableErrorPropagation` directive provides a simple but powerful way for clients to control error propagation behavior. While it doesn't solve all error handling challenges, it represents an important step toward uncoupling nullability and errors in GraphQL. This allows for more precise schema design while maintaining backwards compatibility for existing clients.

0 commit comments

Comments
 (0)