|
| 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