skip to content

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

level: middleimportance: must knowfreq 45%

answer

  1. interface returns Mono<List<GraphQLError>>
  2. Adapter -> resolveToSingleError / resolveToMultipleErrors (sync)
  3. GraphqlErrorBuilder.newError(env) fills path+locations
  4. return null = unresolved -> next resolver
  5. just declare a bean, auto-wired into chain

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.

solid answer

~40 s

The base interface is DataFetcherExceptionResolver, with resolveException(Throwable, DataFetchingEnvironment) returning Mono<List<GraphQLError>> — reactive and low-level. For synchronous code you extend the abstract DataFetcherExceptionResolverAdapter and override resolveToSingleError(ex, env) or resolveToMultipleErrors. Inside, use GraphqlErrorBuilder.newError(env) to build a GraphQLError, set .errorType(ErrorType.NOT_FOUND / BAD_REQUEST / etc.), .message(...), optionally .extensions(...), and .build(). Building from env auto-fills the path and locations. Return null (or empty list) for exceptions you don't handle — that signals 'unresolved' and passes the throwable to the next resolver in the chain; if none resolves it, Spring falls back to the masked INTERNAL_ERROR. Register your resolver as a @Component/@Bean and Spring wires it into the chain automatically.

code

java · 21 lines
java
@Component
public class DomainExceptionResolver extends DataFetcherExceptionResolverAdapter {

    @Override
    protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
        if (ex instanceof BookNotFoundException nf) {
            return GraphqlErrorBuilder.newError(env)   // seeds path + locations
                    .errorType(ErrorType.NOT_FOUND)     // -> extensions.classification
                    .message("Book %d not found".formatted(nf.getId()))
                    .extensions(Map.of("bookId", nf.getId()))
                    .build();
        }
        if (ex instanceof AccessDeniedException) {
            return GraphqlErrorBuilder.newError(env)
                    .errorType(ErrorType.FORBIDDEN)
                    .message("Not allowed")
                    .build();
        }
        return null; // unresolved -> next resolver / masked INTERNAL_ERROR fallback
    }
}

go deeper

for a junior

Know that a bean extending DataFetcherExceptionResolverAdapter maps exceptions to GraphQLErrors.

for a middle

Explain the interface vs adapter, the null-to-pass-through contract, and GraphqlErrorBuilder usage.

for a senior

Distinguish empty-Mono (unresolved) vs empty-list (suppressed), and when to use resolveToMultipleErrors.

for a principal

Weigh a central resolver vs controller-local handlers, and design classification/extensions as a stable client contract.

## The two abstractions Spring for GraphQL exposes exception mapping at two levels: ### 1. `DataFetcherExceptionResolver` (the interface) ```java Mono<List<GraphQLError>> resolveException(Throwable ex, DataFetchingEnvironment env); ``` - **Reactive/low-level.** You return a `Mono` wrapping a list of errors. - Return an **empty Mono** (or `Mono.empty()`) → *unresolved*, meaning 'I don't handle this' → next resolver tries. - Return a Mono of an **empty list** → *resolved to no errors* (you deliberately suppress it). - Return a Mono of a **non-empty list** → those become the errors for that field. Use this directly only when you need reactive/async logic in the mapping. ### 2. `DataFetcherExceptionResolverAdapter` (the convenience base class) Most code extends this abstract class and overrides one of: ```java protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) protected List<GraphQLError> resolveToMultipleErrors(Throwable ex, DataFetchingEnvironment env) ``` - **Synchronous** — no reactive types to deal with. - Return **null** (single) or **null/empty** (multiple) → unresolved → next resolver in the chain. - Return a built `GraphQLError` → resolved. ## Building the error: `GraphqlErrorBuilder` `GraphqlErrorBuilder.newError(env)` seeds the builder from the `DataFetchingEnvironment`, so `path` and `locations` are filled automatically: ```java GraphqlErrorBuilder.newError(env) .errorType(ErrorType.NOT_FOUND) // becomes extensions.classification .message("Book %d not found".formatted(id)) .extensions(Map.of("bookId", id)) // optional custom data .build(); ``` `ErrorType` is Spring's enum (BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, INTERNAL_ERROR) implementing graphql-java's `ErrorClassification`. It surfaces to clients under `extensions.classification`. ## Registration Just declare the resolver as a Spring bean (`@Component` or `@Bean`). Boot's `GraphQlSourceBuilderCustomizer` picks up all `DataFetcherExceptionResolver` beans and wires them into the execution chain. No manual registration needed. ## The 'return null to pass through' contract (key gotcha) The single most common mistake is handling *every* exception in one resolver, or returning a generic error for unknown types. **Return null for exceptions you don't recognize.** This lets the chain try other resolvers (including `@GraphQlExceptionHandler`-derived ones) and finally the built-in masked INTERNAL_ERROR fallback. If you swallow everything, you defeat that layering. ## resolveToSingleError vs resolveToMultipleErrors Override `resolveToMultipleErrors` when one exception should yield several errors — e.g. a bean-validation failure carrying multiple field violations, each mapped to its own GraphQLError with a distinct message. ## When to use this vs @GraphQlExceptionHandler - **Resolver/Adapter** — one central, application-wide place; good for cross-cutting mapping (validation, security, not-found) independent of any controller. - **@GraphQlExceptionHandler** — annotation-driven, can be controller-local, mirrors MVC style. Internally these are adapted into the same resolver chain.

  • What is the difference between returning null and returning an empty list from a resolver?
    Returning null (or empty Mono) means 'unresolved' — the throwable passes to the next resolver, ultimately the masked INTERNAL_ERROR. Returning an empty list means 'resolved to no errors' — you deliberately suppress the error entirely, and the field is just null with no errors entry.
  • Why prefer DataFetcherExceptionResolverAdapter over implementing the interface directly?
    The adapter is synchronous: you override resolveToSingleError/resolveToMultipleErrors and return plain GraphQLError objects instead of wrapping everything in Mono<List<...>>. Implement the interface directly only when the mapping itself needs reactive/async work.

saying these in an interview costs you the question

  • Claiming resolveException returns a plain List instead of Mono<List<GraphQLError>>
  • Handling every exception and never returning null, breaking the resolver chain
  • Thinking you must manually register the resolver in a config method

context