skip to content

In Spring for GraphQL, what happens to the response when a single data fetcher throws an exception?

level: juniorimportance: must knowfreq 42%

answer

  1. per-field data fetchers -> partial results
  2. errors array + data with nulls
  3. path points to failed field
  4. default masks message -> INTERNAL_ERROR
  5. non-null field bubbles null upward

basics

~10 s

Spring catches the exception, sets that field to null, and adds an entry to the top-level "errors" array in the response. Other fields that resolved successfully still return their data — a partial response.

solid answer

~40 s

Unlike REST, GraphQL does not fail the whole request on one error. Each field is resolved by its own DataFetcher. When one throws, Spring for GraphQL's exception-resolver chain converts the throwable into a GraphQLError placed in the response's top-level "errors" array, and the offending field becomes null. Fields that resolved fine keep their values, so you get a partial response: a "data" object (with some nulls) plus an "errors" array. Each error carries a "path" pointing to the field that failed, plus "locations" and "extensions". By default, if no custom resolver handles the exception, Spring returns a generic INTERNAL_ERROR and hides the real message to avoid leaking internals. You customize the mapping with DataFetcherExceptionResolver / DataFetcherExceptionResolverAdapter or @GraphQlExceptionHandler methods.

code

json · 17 lines
json
// Query:
// { book(id: 1) { title author { name } } }
//
// author's data fetcher throws; title succeeds -> partial response:
{
  "data": {
    "book": { "title": "Dune", "author": null }
  },
  "errors": [
    {
      "message": "INTERNAL_ERROR for 3f2a-...",   // masked by default
      "path": ["book", "author"],
      "locations": [{ "line": 1, "column": 22 }],
      "extensions": { "classification": "INTERNAL_ERROR" }
    }
  ]
}

go deeper

for a junior

Know that GraphQL returns partial results: failed field becomes null, an entry goes into the errors array, and other fields still return data.

for a middle

Explain the GraphQLError anatomy (message/path/locations/extensions) and the default masking to INTERNAL_ERROR.

for a senior

Discuss non-null propagation and why masking is a security default, plus how to override it.

for a principal

Frame partial errors as a client-contract decision: which failures degrade gracefully vs. which should null the whole payload, and how error classification drives client behavior.

## The core difference from REST In a typical REST controller, an unhandled exception maps to a single HTTP status and body — the whole call is a failure. **GraphQL is different**: a single query can ask for many fields, and each field is produced by its own *data fetcher* (the function that returns a field's value). Because of this, GraphQL supports **partial results** — some fields can succeed while others fail. ## What Spring does when a data fetcher throws Spring for GraphQL (the `spring-graphql` project, auto-configured by `spring-boot-starter-graphql`) wraps every data-fetcher invocation. When one throws a `Throwable`: 1. The throwable is passed through a chain of **`DataFetcherExceptionResolver`** beans. 2. The chain converts it into one or more **`GraphQLError`** objects (a graphql-java type). 3. Those errors are added to the response's top-level **`errors`** array. 4. The field that failed is set to **`null`** in the **`data`** object. The result is a JSON shape like: ```json { "data": { "book": { "title": "Dune", "author": null } }, "errors": [ { "message": "...", "path": ["book","author"], "locations": [...], "extensions": { "classification": "INTERNAL_ERROR" } } ] } ``` ## Anatomy of a GraphQLError - **`message`** — human-readable text. By default Spring **masks** it for unresolved exceptions (see below). - **`path`** — the field location in the query, e.g. `["book","author"]`. Lets clients know *which* field failed. - **`locations`** — line/column in the query document. - **`extensions`** — a free-form map. Spring puts the error **classification** (from `ErrorType`) under `extensions.classification`. ## The default masking behavior (important gotcha) If **no** custom resolver handles the exception, Spring's built-in handling logs the real exception server-side and returns a **generic** error with classification `INTERNAL_ERROR` and a message like `"INTERNAL_ERROR for <executionId>"`. The real exception message is deliberately **hidden** so you don't leak stack traces, SQL, or internal details to clients. To surface a meaningful message you must **explicitly** map the exception (via a resolver or `@GraphQlExceptionHandler`). ## Non-nullable fields propagate the null upward If the failed field is declared **non-nullable** (`Author!` in the schema), GraphQL cannot put `null` there, so the `null` **bubbles up** to the nearest nullable parent. In the worst case (`Query.book: Book!` and everything non-null), the entire `data` becomes `null`. This is standard graphql-java null-propagation, not a Spring-specific rule, but it explains why sometimes you *don't* get a partial result. ## When to use what - **Do nothing** → generic masked INTERNAL_ERROR (safe default, poor UX). - **`DataFetcherExceptionResolverAdapter`** → central, type-based mapping across all controllers. - **`@GraphQlExceptionHandler`** → annotation-driven, controller-local or global via `@ControllerAdvice`, mirrors Spring MVC's `@ExceptionHandler` style.

  • Why is the real exception message hidden by default?
    Security. Unhandled exceptions could leak stack traces, SQL, or internal details to clients, so Spring logs the real error server-side and returns a generic masked message with classification INTERNAL_ERROR. You opt in to exposing messages by explicitly mapping the exception.
  • If author is declared Author! (non-null) and its fetcher fails, is the response still partial?
    Not at that position — GraphQL can't put null in a non-null field, so the null propagates up to the nearest nullable ancestor. If every ancestor up to Query is non-null, the whole data becomes null while the error still appears in the errors array.

context