skip to content

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

level: seniorimportance: should knowfreq 38%

answer

  1. @ExceptionHandler-style, adapted by AnnotatedControllerConfigurer
  2. in @Controller = local; in @ControllerAdvice = global
  3. precedence: local -> advice -> resolver beans -> masked default
  4. returns GraphQLError / List / void
  5. parse/validate errors NOT routed here

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.

solid answer

~30 s

@GraphQlExceptionHandler is the annotation-driven, Spring-MVC-style alternative to writing a DataFetcherExceptionResolver. You annotate a method whose parameter type selects the exception it handles; it can return a GraphQLError, a List<GraphQLError>, Object, or void, and can inject DataFetchingEnvironment for path/locations. Placed inside a @Controller, it only handles exceptions from that controller's data fetchers (controller-local, highest precedence). Placed inside a @ControllerAdvice bean, it handles exceptions from all controllers (global). The AnnotatedControllerConfigurer adapts these methods into the same DataFetcherExceptionResolver chain, so precedence is: controller-local handlers first, then @ControllerAdvice, then standalone DataFetcherExceptionResolver beans, then the built-in masked INTERNAL_ERROR. Method-matching mirrors @ExceptionHandler: most specific exception type wins.

code

java · 35 lines
java
@ControllerAdvice
public class GraphQlErrorAdvice {

    // Global: applies to exceptions from ALL controllers' data fetchers
    @GraphQlExceptionHandler
    public GraphQLError handleNotFound(EntityNotFoundException ex,
                                       DataFetchingEnvironment env) {
        return GraphqlErrorBuilder.newError(env)
                .errorType(ErrorType.NOT_FOUND)
                .message(ex.getMessage())
                .build();
    }

    // Multiple errors from one validation failure
    @GraphQlExceptionHandler
    public List<GraphQLError> handleValidation(ConstraintViolationException ex,
                                               DataFetchingEnvironment env) {
        return ex.getConstraintViolations().stream()
                .map(v -> GraphqlErrorBuilder.newError(env)
                        .errorType(ErrorType.BAD_REQUEST)
                        .message(v.getPropertyPath() + ": " + v.getMessage())
                        .build())
                .toList();
    }
}

@Controller
class BookController {
    // Controller-local: shadows the global advice for BookController's fetchers
    @GraphQlExceptionHandler
    GraphQLError handle(BookLockedException ex, DataFetchingEnvironment env) {
        return GraphqlErrorBuilder.newError(env)
                .errorType(ErrorType.FORBIDDEN).message("Book is locked").build();
    }
}

go deeper

for a junior

Know it's a method annotation that maps an exception to a GraphQLError, similar to MVC's @ExceptionHandler.

for a middle

Explain controller-local vs @ControllerAdvice scope and that it's adapted into the resolver chain.

for a senior

Nail the full precedence order and the fact that parse/validation errors are out of scope.

for a principal

Design a layered strategy: global advice for domain mapping, controller-local for exceptions, resolvers for reactive/cross-cutting concerns, with a documented classification contract.

## What it is `@GraphQlExceptionHandler` brings the familiar Spring MVC `@ExceptionHandler` programming model to GraphQL. Instead of implementing `DataFetcherExceptionResolver`, you write annotated methods; the **`AnnotatedControllerConfigurer`** (the same component that turns `@SchemaMapping`/`@QueryMapping` methods into data fetchers) adapts them into the resolver chain. ## Method signature flexibility The handler method: - **Selects the exception** by its parameter type: `handle(BookNotFoundException ex)` handles that type and subtypes. - **Can inject** `DataFetchingEnvironment` (for path/locations), and other supported arguments. - **Return types** allowed: `GraphQLError`, `List<GraphQLError>`, `Collection<GraphQLError>`, `Object`, or `void`. Returning `void`/`null` typically means 'not resolved here'. ```java @GraphQlExceptionHandler public GraphQLError handle(BookNotFoundException ex, DataFetchingEnvironment env) { return GraphqlErrorBuilder.newError(env) .errorType(ErrorType.NOT_FOUND) .message(ex.getMessage()) .build(); } ``` You can also narrow with `@GraphQlExceptionHandler(SomeException.class)` on the annotation instead of the parameter. ## Two placements — two scopes ### Controller-local A `@GraphQlExceptionHandler` method **inside a `@Controller`** only handles exceptions raised by **that controller's** data fetchers. This is the most specific and has the **highest precedence**. ### Global via `@ControllerAdvice` A `@GraphQlExceptionHandler` method **inside a `@ControllerAdvice`** (or `@ControllerAdvice`-meta-annotated) bean handles exceptions from **all** controllers — the cross-cutting, application-wide layer. ## Precedence / resolution order When a data fetcher throws, Spring tries handlers in this order: 1. **Controller-local** `@GraphQlExceptionHandler` methods (same controller as the failing fetcher). 2. **`@ControllerAdvice`** global `@GraphQlExceptionHandler` methods. 3. Standalone **`DataFetcherExceptionResolver`** beans (including `...Adapter` subclasses). 4. Built-in fallback → masked **`INTERNAL_ERROR`**. Within a class, exception matching follows the same 'most specific supertype wins' logic as MVC's `@ExceptionHandler`. ## Gotchas - **Handlers only fire for data-fetcher exceptions.** Exceptions during document parsing/validation (before execution) are *not* routed here — those are handled differently (e.g. `WebGraphQlInterceptor` / instrumentation), and there is no `path` because no field executed. - **Returning null/void = unresolved**, so the next handler layer runs — same pass-through idea as resolvers. - **Ordering among controller vs advice matters**: a controller-local handler shadows a global one for the same exception type. - Handlers can return **multiple** errors (`List<GraphQLError>`), useful for validation. ## When to use which - Use **controller-local** when a specific controller needs bespoke mapping. - Use **`@ControllerAdvice`** for consistent, app-wide domain-to-error mapping (recommended default for most teams). - Use a **`DataFetcherExceptionResolver` bean** when you want mapping fully decoupled from the annotation/controller model or need reactive logic.

  • What is the precedence order when both a controller-local handler and a @ControllerAdvice handler match the same exception?
    The controller-local @GraphQlExceptionHandler wins — it's most specific to the failing fetcher's controller. Order overall is controller-local, then @ControllerAdvice global, then standalone DataFetcherExceptionResolver beans, then the built-in masked INTERNAL_ERROR.
  • Are schema validation or query-parsing errors handled by @GraphQlExceptionHandler?
    No. These occur before field execution, so there's no data fetcher and no path. They're surfaced as validation errors by graphql-java and can be shaped via interceptors/instrumentation, not the exception-handler chain.

saying these in an interview costs you the question

  • Saying @GraphQlExceptionHandler handles parsing/validation errors too
  • Claiming @ControllerAdvice handlers take precedence over controller-local ones
  • Thinking it can only return a single GraphQLError, not a list

context