Skip to main content

onError: Letting the Client Decide How Far a GraphQL Error Spreads

· 9 min read
GraphQL Guy

onError and error behaviors

A movie has a title and a rating. The rating service is down, so the rating field fails, and the title loaded without any problem. Under today's GraphQL rules, the client may still lose the whole movie, because one failed field can take its parent object with it. For years, the community answer has been "make more fields nullable". The proposal now moving through the GraphQL working group answers differently: let the client say, in each request, how far an error is allowed to spread. It is called onError, and in August 2026 the working group also decided what happens to every client that does not send it.

One Failed Field, One Missing Movie​

Here is the schema we used, on Spring Boot 4.1.1 with graphql-java 25.0. The data fetcher for rating throws an exception to simulate the broken rating service:

schema.graphqls
type Query {
movie(id: ID!): Movie
}

type Movie {
id: ID!
title: String!
rating: Float!
}

And here is what a client gets back when it asks for both fields. We sent the query on one line, which is why the error location says line 1, and we formatted the response for reading:

Request
query MovieRating {
movie(id: "1") {
title
rating
}
}
Response
{
"errors": [
{
"message": "INTERNAL_ERROR for 7c058edf-3379-620d-1d41-cd3b2e39d60e",
"locations": [{ "line": 1, "column": 44 }],
"path": ["movie", "rating"],
"extensions": { "classification": "INTERNAL_ERROR" }
}
],
"data": { "movie": null }
}

The title was loaded, and the response does not contain it. rating is declared Float!, so the server may not return null there. The specification keeps that promise by passing the error up to the parent: movie becomes null, because movie is nullable. Had movie been non-null too, the error would have continued up to data itself. This is error propagation, and it is the behavior the specification requires today.

Why Everyone Made Everything Nullable​

The usual defense is to make fields nullable, so that an error stops at the field where it happened. The graphql.org guidance has recommended that for years, and our fifty shades of null post explains the trade-off in detail.

It works, and it has a cost. A nullable rating now means two things: "this movie is unrated" and "the rating service failed". Applied to every field, the habit means the schema can no longer say that every movie has a title. Every client has to check for null everywhere, including in places where null can only mean an error. The error behaviors proposal lists two deeper problems with propagation:

  1. It destroys useful data in the response.
  2. It makes it unsafe to store resulting data in normalized stores.

The second point matters for clients like Apollo Client, Relay and urql, which keep a normalized cache. If an error at movie.rating stores movie: null in the cache, other parts of the application that read movie(id: "1") from the cache get null as well, even though the movie exists.

Three Error Behaviors​

PR #1163, written by Benjie, adds an onError property to the request. It sits next to query and variables in the request body:

A request with onError
{
"query": "query MovieRating { movie(id: \"1\") { title rating } }",
"onError": "NULL"
}

The property takes one of three values:

  • PROPAGATE is the behavior described above. The error moves up to the nearest parent position that may be null.
  • NULL turns propagation off. The failed position becomes null, even when the schema says it is non-null, and the error in the errors list marks it. Everything else stays in the response.
  • HALT stops execution at the first execution error. The response holds that one error and "data": null. This is for clients that discard the whole response when anything fails, so the server can save the work.

With NULL, a null in a non-null position always means an error happened there, and the proposal says clients must read the errors list to tell the two kinds of null apart. That rule gives a schema back its ability to describe the data truthfully. title and rating can be String! and Float! because every movie has them, and failures stay where they happened.

You can see what NULL would produce today. graphql-java and graphql-js both ship an experimental @experimental_disableErrorPropagation directive that works like onError: "NULL". Here is the same query with it, on the same server:

Request
query MovieRating @experimental_disableErrorPropagation {
movie(id: "1") {
title
rating
}
}
Response
{
"errors": [
{
"message": "INTERNAL_ERROR for 64bff046-d17c-c170-a0ed-ef982e6c0222",
"locations": [{ "line": 1, "column": 82 }],
"path": ["movie", "rating"],
"extensions": { "classification": "INTERNAL_ERROR" }
}
],
"data": { "movie": { "title": "The Matrix", "rating": null } }
}

The title survives, the rating is null, and the error's path points at that position.

PROPAGATE Is the Default, Permanently​

The most important decision about onError concerns the requests that do not use it. An earlier version of the proposal let a new service choose a different default. At the working group meeting on 6 August 2026, that option went away, and the pull request now records the outcome:

Per the working group on 2026-08-06 we will forever have PROPAGATE as the default error behavior, so this capability has been removed.

Keeping the default protects every client that already exists. Generated types in many applications say that a String! field always holds a string. A server that switched its default to NULL would send null into those positions, and the code reading them would fail at runtime. With PROPAGATE as the permanent default, nothing changes for a client until the client asks for it.

The same meeting recorded the other half of the plan. According to the meeting summary, new clients such as urql and Apollo Client should send onError: "NULL" whenever the server supports it. Old clients keep their behavior, and new clients get the better one.

How a Client Knows the Server Supports It​

That plan needs one more piece, because a server that does not know onError ignores it. We sent "onError": "NULL" to our Spring Boot 4.1.1 application, and the response still had "movie": null. It looked the same as a response to a request without the property. A client that assumes NULL behavior from a server that propagates errors will misread the response.

PR #1208 proposes service capabilities to close that gap: a new __service introspection field through which a server lists what it supports, outside the type system. A client could ask:

query ServiceCapabilities {
__service {
capabilities {
identifier
value
}
}
}

A server that honors onError would list the graphql.onError capability. Service capabilities are useful beyond errors, too. The pull request mentions fragment arguments, incremental delivery and subscription transports as other things a client could discover this way, and it is part of the Golden Path goal of clients that configure themselves.

There is also a second, smaller pull request. PR #1236, by Martin Bonnin, specifies onError on its own, without service capabilities, so that the two can move at different speeds.

Who Supports It Today​

In September 2026, PR #1163 and PR #1208 are at RFC 1, and implementations are arriving ahead of the specification:

  • Apollo Kotlin merged client support for onError in June 2026.
  • Hot Chocolate merged support for the new error handling modes in its Fusion gateway in August 2025.
  • graphql-js and graphql-java have open pull requests for onError (#4842 and #4378), and both already ship the experimental directive shown above.
  • Relay has handled errors per fragment since Relay 18 in 2024, with @catch and @throwOnFieldError, and much of this design started there.

For schemas, the step available now is @semanticNonNull, a GAP proposal by Martin Bonnin. It marks a field that is non-null unless an error occurred:

type Movie {
title: String @semanticNonNull
rating: Float @semanticNonNull
}

Old clients see an ordinary nullable field. Clients that understand the directive can generate non-null types and handle errors themselves. graphql-sock converts such a schema into a strict version or a fully nullable version, for tools that need one or the other.

What to Do Now​

Nothing here forces a change, because PROPAGATE stays the default. If you want to prepare, each role has a small step:

  • Schema authors: mark fields that the domain always fills with @semanticNonNull. Many of them are nullable today only as a precaution against errors.
  • Client developers: follow your client library's releases, and turn on onError: "NULL" once both your client and your server support it.
  • Server developers: try @experimental_disableErrorPropagation in a test environment to see how your clients cope with errors that stay in place.

Class 8 of our error handling course walks through the same proposals with more examples, together with @semanticNonNull and graphql-sock.


This post contains at least one mistake that we have not found yet. Read with onError: "NULL", you still get every other paragraph. Read with PROPAGATE, the error would travel up to the title, and you would be looking at an empty page with one error message underneath.

Sources​