What is @GraphQlExceptionHandler and how does controller-local versus global (@ControllerAdvice) handling differ?
answer
- @ExceptionHandler-style, adapted by AnnotatedControllerConfigurer
- in @Controller = local; in @ControllerAdvice = global
- precedence: local -> advice -> resolver beans -> masked default
- returns GraphQLError / List / void
- 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@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
Know it's a method annotation that maps an exception to a GraphQLError, similar to MVC's @ExceptionHandler.
Explain controller-local vs @ControllerAdvice scope and that it's adapted into the resolver chain.
Nail the full precedence order and the fact that parse/validation errors are out of scope.
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