Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
31c90e7
Detail onError request parameter
benjie Apr 30, 2025
f4fab96
Detail introspection changes
benjie Apr 30, 2025
692d811
Define the directive
benjie Apr 30, 2025
94446ab
ABORT -> HALT
benjie May 15, 2025
3c63355
Start speccing out the capabilities system
benjie May 15, 2025
7056690
Add a number of basic capabilities
benjie May 15, 2025
0fa7a33
Move default error behavior to the service
benjie May 15, 2025
1f975e4
Rework capabilities
benjie May 22, 2025
8c40086
Use a definition
benjie May 22, 2025
641a786
Reorder
benjie May 22, 2025
026982b
Reword
benjie May 22, 2025
a7c6ad5
Editorial
benjie May 22, 2025
fe559ea
More editorial
benjie May 22, 2025
b5f64ae
More editorial
benjie May 22, 2025
1c3f0cd
Update spec/Section 4 -- Introspection.md
benjie May 28, 2025
7ab36b8
Merge branch 'main' into error-behavior2
benjie Jul 10, 2025
cc50991
Change NO_PROPAGATE to NULL
benjie Jul 10, 2025
df977eb
Overhaul and introduce SDL syntax for service capabilities
benjie Aug 29, 2025
b1f039c
Update syntax
benjie Aug 29, 2025
b28ef2f
Simplify
benjie Aug 29, 2025
144e854
Update Name syntax
benjie Aug 29, 2025
dc9315c
Clarification
benjie Aug 29, 2025
6fd7239
Minor tweaks
benjie Aug 29, 2025
c44a7ae
Clarify the interaction of onError:HALT and subscriptions
benjie Aug 29, 2025
aef7069
Fix grammar
benjie Aug 29, 2025
955acd4
Spelling
benjie Aug 29, 2025
2fc8b0c
Fix incorrect prefix
benjie Oct 28, 2025
aef2e2d
Remove unnecessary negative lookahead
benjie Nov 6, 2025
4d2a94b
Lee says empty braces are okay here!
benjie Nov 6, 2025
bbe2512
Merge branch 'main' into error-behavior2
benjie Nov 6, 2025
55458b1
Sync syntax with Lee's suggestions
benjie Nov 6, 2025
edd5df0
Merge branch 'main' into error-behavior2
benjie Aug 3, 2026
27317f8
Remove service capabilities; now in #1208
benjie Aug 3, 2026
8d08585
Add pathNonNull so clients can reproduce propagation locally
benjie Aug 3, 2026
f5e5ffd
Add pathNonNull to examples
benjie Aug 3, 2026
26b4aa5
Better wording
benjie Aug 3, 2026
39084dd
No capabilities
benjie Aug 3, 2026
22e8ea8
Remove the concept of a default error behavior
benjie Aug 6, 2026
67fea0d
Less awkward wording, per Lee
benjie Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 77 additions & 21 deletions spec/Section 6 -- Execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ A GraphQL service generates a response from a request via execution.
- {schema}: The schema to use, typically solely provided by the GraphQL service.
- {document}: A {Document} which must contain GraphQL {OperationDefinition} and
may contain {FragmentDefinition}.
- {onError} (recommended): The _error behavior_ to apply to the request; see
[Handling Execution Errors](#sec-Handling-Execution-Errors). Clients should
provide {onError} as part of a GraphQL request. If {onError} is provided and
its value is not one of {"NULL"}, {"PROPAGATE"}, or {"HALT"}, then a _request
error_ must be raised. If {onError} is not provided, the value {"PROPAGATE"}
will be used.
- {operationName} (optional): The name of the Operation in the Document to
execute.
- {variableValues} (optional): Values for any Variables defined by the
Expand All @@ -22,6 +28,10 @@ Given this information, the result of {ExecuteRequest(schema, document,
operationName, variableValues, initialValue)} produces the response, to be
formatted according to the Response section below.

Note: Previous versions of this specification did not define the {onError}
request attribute. Clients should only include {onError} in the request if it is
known that the service supports this property.

Implementations should not add additional properties to a _request_, which may
conflict with future editions of the GraphQL specification. Instead,
{extensions} provides a reserved location for implementation-specific additional
Expand Down Expand Up @@ -600,13 +610,26 @@ section.
</a>

If during {ExecuteCollectedFields()} a _response position_ with a non-null type
raises an _execution error_ then that error must propagate to the parent
response position (the entire selection set in the case of a field, or the
entire list in the case of a list position), either resolving to {null} if
allowed or being further propagated to a parent response position.

If this occurs, any sibling response positions which have not yet executed or
have not yet yielded a value may be cancelled to avoid unnecessary work.
raises an _execution error_, the error must be added to the {"errors"} list in
the _execution result_ and then handled according to the _error behavior_ of the
request:

- {"NULL"}: The _response position_ must be set to {null}, even if such position
is indicated by the schema to be non-nullable. (The client is responsible for
interpreting this {null} in conjunction with the {"errors"} list to
distinguish error results from intentional {null} values.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case of mutation resolution, does "NULL" mean execution continues?

eg if I have a mutation:

mutation {
  doThing1
  doThing2
  doThing3
}

and all fields were not nullable, which of these responses would be correct in the presence of an error on resolution of doThing2:

A: response fields deviate from graphql request ask

{
  "data": {
    "doThing1": true,
    "doThing2": null
    /* doThing3 never resolved */
  },
  "errors": [
    { /* error from doThing2 */ }
  ]
}

B: synthetic field production - propagate null to down-operation resolution

{
  "data": {
    "doThing1": true,
    "doThing2": null,
    "doThing3": null /* never resolved but server injects null because field is requested */
  },
  "errors": [
    { /* error path doThing2 */ }
  ]
}

C: continue resolving

{
  "data": {
    "doThing1": true,
    "doThing2": null,
    "doThing3": null /* server application's responsibility for stopping execution after error */
  },
  "errors": [
    { /* error path doThing2 */ },
    { /* error path doThing3 thrown due to response errored flag set, does not indicate a problem with doThing3 */ }
  ]
}

or perhaps more confusingly

{
  "data": {
    "doThing1": true,
    "doThing2": null,
    "doThing3": true
  },
  "errors": [
    { /* error from doThing2 */ }
  ]
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The final “perhaps more confusingly” is the correct behavior according to the current semantics. It’s an interesting question, because if the client changes the onError without the knowledge of the user, the resulting side-effects will differ. Option C is interesting, but also wrong in its own way. In my own schemas, root level fields (even mutations) are nullable so it wouldn’t make a difference to me, I wonder if making the fields non-nullable is done specifically to block follow-up mutations currently…? Though it does mean you wouldn’t see the result of previously completed mutations so it seems like a weird choice.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hot Chocolate's mutation convention makes mutations not nullable and it is quite annoying to change: https://chillicream.com/docs/hotchocolate/v16/building-a-schema/mutations/#mutation-conventions and I think semantically it is correct (the mutation always has a result or there is an error).

I agree the current wording suggests option C as well (both of the last 2 samples are option C). The difference between them is if the server implementer (or framework they use) flag that an error occurred via some mechanism such that if inside ExecuteMutation() the flag is set, every following ExecuteSelectionSet() immediately raises an error.

I suppose my point is that either application authors (both clients and servers) will need to beware of of the possibility that this could happen (and frameworks could take an opinionated stance or provide a default) or the spec force A or B and clients (and potentially client libraries) would need to be able to handle such responses. I don't think there is a satisfying answer here.

@martinbonnin martinbonnin May 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bbarry can you ellaborate why C. is not satisfying?

onError: NULL makes it possible to resolve more data, whether for queries or subscriptions. Forcing A. or B. would be very surprising to me. With C. the server can decide how it wants to handle things. If anything, we could recommend to have only a single root field in mutations. I think this was discussed at some point but can't find it anymore.

Edit: found it! It's "batched mutations" article: https://medium.com/@xuorig/graphql-mutation-design-batch-updates-ca2452f92833

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is a very hard problem and every solution I've seen so far is unsatisfying in some way. In each case there is room for interpretation and miscommunication between client and server. If clients could just send valid data and servers could avoid flaky systems the world would be a much simpler place.

What I find unsatisfying about C is that a client a client sending such a mutation operation could intend that the 3 mutation fields are causality related. If the second doesn't happen and the 3rd does, there could be a logic bug in the client. If the server author chooses to avoid this potential by raising down-field errors the server is adding unnecessary response content and falsely stating there is a problem with a field when in fact the problem is outside the field.

- {"PROPAGATE"}: The _execution error_ must propagate to the parent _response
position_ (the entire selection set in the case of a field, or the entire list
in the case of a list position). The parent position resolves to {null} if
allowed, or else the error is further propagated to a parent response
position. Any sibling response positions that have not yet executed or have
not yet yielded a value may be cancelled to avoid unnecessary work.
- {"HALT"}: The current {ExecuteRootSelectionSet()} must be aborted immediately
and must yield an execution result with an {"errors"} list consisting of this
_execution error_ only and the {"data"} entry set to {null}. Any _response
position_ that has not yet executed or has not yet yielded a value may be
cancelled to avoid unnecessary work. (Note: For a subscription operation the
underlying stream is not terminated.)

Note: See [Handling Execution Errors](#sec-Handling-Execution-Errors) for more
about this behavior.
Expand Down Expand Up @@ -902,29 +925,52 @@ ResolveAbstractType(abstractType, objectValue):
</a>

An _execution error_ is an error raised during field execution, value resolution
or coercion, at a specific _response position_. While these errors must be
reported in the response, they are "handled" by producing partial {"data"} in
the _response_.
or coercion, at a specific _response position_. These errors must be added to
the {"errors"} list in the _response_, and are "handled" according to the _error
behavior_ of the request.

Note: An _execution error_ is distinct from a _request error_ which results in a
response with no {"data"}.

If a _response position_ resolves to {null} because of an execution error which
has already been added to the {"errors"} list in the _execution result_, the
{"errors"} list must not be further affected. That is, only one error should be
added to the errors list per _response position_.

:: The _error behavior_ of a request indicates how an _execution error_ is
handled; valid values are {"NULL"}, {"PROPAGATE"} and {"HALT"}. The _error
behavior_ for a _request_ should be specified using the {onError} attribute of
the request; if unspecified, the _error behavior_ is {"PROPAGATE"}.

Regardless of error behavior, if a _response position_ with a non-null type
results in {null} due to the result of {ResolveFieldValue()} then an execution
error must be raised at that position as specified in {CompleteValue()}.

The _error behavior_ of a request applies to every _execution error_ raised
during execution.

The following sections describe the behavior of each valid value:

Note: This is distinct from a _request error_ which results in a _request error
result_ with no data.
**{"NULL"}**

If an execution error is raised while resolving a field (either directly or
nested inside any lists), it is handled as though the _response position_ at
which the error occurred resolved to {null}, and the error must be added to the
{"errors"} list in the _execution result_.
With {"NULL"}, a `Non-Null` _response position_ will have the value {null} if
and only if an error occurred at that position.

Note: Clients must inspect the {"errors"} list and use the {"path"} of each
error result to distinguish between intentional {null} values and those
resulting from an _execution error_.

**{"PROPAGATE"}**

With {"PROPAGATE"}, a `Non-Null` _response position_ must not contain {null} in
the _response_.

If the result of resolving a _response position_ is {null} (either due to the
result of {ResolveFieldValue()} or because an execution error was raised), and
that position is of a `Non-Null` type, then an execution error is raised at that
position. The error must be added to the {"errors"} list in the _execution
result_.

If a _response position_ resolves to {null} because of an execution error which
has already been added to the {"errors"} list in the _execution result_, the
{"errors"} list must not be further affected. That is, only one error should be
added to the errors list per _response position_.

Since `Non-Null` response positions cannot be {null}, execution errors are
propagated to be handled by the parent _response position_. If the parent
response position may be {null} then it resolves to {null}, otherwise if it is a
Expand All @@ -939,3 +985,13 @@ position_ must resolve to {null}. If the `List` type is also wrapped in a
If every _response position_ from the root of the request to the source of the
execution error has a `Non-Null` type, then the {"data"} entry in the _execution
result_ should be {null}.

**{"HALT"}**

With {"HALT"}, {ExecuteRootSelectionSet()} must cease immediately that the first
_execution error_ is raised. That error must be added to the {"errors"} list,
and {"data"} must be {null}.

Note: For subscription operations, processing of the current event is ceased,
but the subscription still remains in place and future events will be processed
as normal.
49 changes: 43 additions & 6 deletions spec/Section 7 -- Response.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@ found at `["hero", "friends"]`, the hero's first friend at
`["hero", "friends", 0]` and that friend's name at
`["hero", "friends", 0, "name"]`.

### Response Path Nullability

Every non-empty prefix of a _response path_ is itself a response path and
identifies a _response position_. A _response position_ is non-null if the type
at that position is a Non-Null type.

:: The _response path nullability_ of a _response path_ is a list of boolean
values having the same length as the response path. Each value corresponds to
the _response position_ identified by the _response path_ prefix ending at the
same index: the value is {true} if that response position is non-null, and
{false} otherwise.

### Data

The {"data"} entry in the _execution result_ will be the result of the execution
Expand Down Expand Up @@ -166,7 +178,9 @@ An execution error is typically the fault of a GraphQL service.

An _execution error_ must occur at a specific _response position_, and may occur
in any response position. The response position of an execution error is
indicated via a _response path_ in the error response's {"path"} entry.
indicated via a _response path_ in the error response's {"path"} entry. The
_response path nullability_ of that response path is indicated via the error's
{"pathNonNull"} entry.

When an execution error is raised at a given _response position_, then that
response position must not be present within the _response_ {"data"} entry
Expand All @@ -191,13 +205,18 @@ If an error can be associated to a particular field in the GraphQL result, it
must contain an entry with the key {"path"} with a _response path_ which
describes the _response position_ which raised the error. This allows clients to
identify whether a {null} resolved result is a true value or the result of an
_execution error_.
_execution error_. It must also contain an entry with the key {"pathNonNull"}
with the _response path nullability_ for that path. This enables clients to
implement advanced error handling behavior: for example, a client could issue a
request using the {"NULL"} _error behavior_ and then reproduce any _error
behavior_ locally, something that would otherwise require access to both the
schema and the request document.

For example, if fetching one of the friends' names fails in the following
operation:

```graphql example
{
query episodeHero($episode: Int!) {
hero(episode: $episode) {
name
heroFriends: friends {
Expand All @@ -208,6 +227,20 @@ operation:
}
```

Against the following schema:

```graphql example
type Query {
hero(episode: Int!): Hero
}

type Hero {
id: ID!
name: String
friends: [Hero]!
}
```

The response might look like:

```json example
Expand All @@ -216,7 +249,8 @@ The response might look like:
{
"message": "Name for character with ID 1002 could not be fetched.",
"locations": [{ "line": 6, "column": 7 }],
"path": ["hero", "heroFriends", 1, "name"]
"path": ["hero", "heroFriends", 1, "name"],
"pathNonNull": [false, true, false, false]
}
],
"data": {
Expand Down Expand Up @@ -248,15 +282,16 @@ raised, even if that field is not present in the response.

For example, if the `name` field from above had declared a `Non-Null` return
type in the schema, the result would look different but the error reported would
be the same:
be the same except the corresponding entry in {"pathNonNull"}:

```json example
{
"errors": [
{
"message": "Name for character with ID 1002 could not be fetched.",
"locations": [{ "line": 6, "column": 7 }],
"path": ["hero", "heroFriends", 1, "name"]
"path": ["hero", "heroFriends", 1, "name"],
"pathNonNull": [false, true, false, true]
}
],
"data": {
Expand Down Expand Up @@ -290,6 +325,7 @@ see fit, and there are no additional restrictions on its contents.
"message": "Name for character with ID 1002 could not be fetched.",
"locations": [{ "line": 6, "column": 7 }],
"path": ["hero", "heroFriends", 1, "name"],
"pathNonNull": [false, true, false, false],
"extensions": {
"code": "CAN_NOT_FETCH_BY_ID",
"timestamp": "Fri Feb 9 14:33:09 UTC 2018"
Expand All @@ -314,6 +350,7 @@ discouraged.
"message": "Name for character with ID 1002 could not be fetched.",
"locations": [{ "line": 6, "column": 7 }],
"path": ["hero", "heroFriends", 1, "name"],
"pathNonNull": [false, true, false, false],
"code": "CAN_NOT_FETCH_BY_ID",
"timestamp": "Fri Feb 9 14:33:09 UTC 2018"
}
Expand Down