skip to content

Error Handling

GraphQL returns partial data with per-field errors, so exception resolvers map your exceptions into typed GraphQL errors rather than an HTTP status. Interviewers ask how error handling differs from REST, and 'the response is still 200' is the headline.

part ofSpring for GraphQLoverview, primer and where to startread it →
on this pageshow

questions

5

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

open as a page

How do you map a custom exception to a specific GraphQLError using DataFetcherExceptionResolver / DataFetcherExceptionResolverAdapter?

level: middleimportance: must knowfreq 45%

basics

~10 s

Register a bean that extends DataFetcherExceptionResolverAdapter and override resolveToSingleError (or resolveToMultipleErrors). Build a GraphQLError with GraphqlErrorBuilder, set an ErrorType and message. Return null for exceptions you don't handle so the next resolver tries.

open as a page

What is Spring's ErrorType enum, how does it surface to clients, and why does an unhandled exception show as INTERNAL_ERROR?

level: seniorimportance: should knowfreq 34%

basics

~10 s

ErrorType is Spring's enum of error categories (BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, INTERNAL_ERROR) implementing graphql-java's ErrorClassification. It appears to clients under extensions.classification. Unhandled exceptions default to INTERNAL_ERROR with a masked message for security.

open as a page

What is @GraphQlExceptionHandler and how does controller-local versus global (@ControllerAdvice) handling differ?

level: seniorimportance: should knowfreq 38%

basics

~20 s

@GraphQlExceptionHandler marks a method that maps a matching exception to a GraphQLError (or list). Put it in a @Controller for that controller's fetchers only, or in a @ControllerAdvice class to handle exceptions globally across all controllers.

open as a page

Explain the full exception-resolution chain in Spring for GraphQL and how you design partial per-field error behavior for a list query.

level: principalimportance: should knowfreq 22%

basics

~20 s

When a fetcher throws, Spring runs an ordered resolver chain (controller-local @GraphQlExceptionHandler, then @ControllerAdvice, then DataFetcherExceptionResolver beans, then the masked INTERNAL_ERROR default). Each error carries a path, so a failing element in a list produces one error while sibling elements still return data — a partial response.

open as a page