quarkusio/quarkus · error · IllegalStateException
@ExceptionHandler methods in @ControllerAdvice must be publi
Error message
@ExceptionHandler methods in @ControllerAdvice must be public instance methods
What it means
Quarkus scans @ControllerAdvice classes for @ExceptionHandler methods and generates ExceptionMapper classes for them via bytecode recording. Only public, non-static instance methods can be wrapped into JAX-RS mappers, so a method that is non-public or static causes a build-time failure.
Source
Thrown at extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/SpringWebProcessor.java:216
BuildProducer<ReflectiveClassBuildItem> reflectiveClassProducer,
BuildProducer<UnremovableBeanBuildItem> unremovableBeanProducer, IndexView index, ClassOutput classOutput,
TypesUtil typesUtil, boolean isResteasyClassic) {
AnnotationInstance controllerAdviceInstance = getSingleControllerAdviceInstance(index);
if (controllerAdviceInstance == null) {
return;
}
ClassInfo controllerAdvice = controllerAdviceInstance.target().asClass();
List<MethodInfo> methods = controllerAdvice.methods();
for (MethodInfo method : methods) {
AnnotationInstance exceptionHandlerInstance = method.annotation(EXCEPTION_HANDLER);
if (exceptionHandlerInstance == null) {
continue;
}
if (!Modifier.isPublic(method.flags()) || Modifier.isStatic(method.flags())) {
throw new IllegalStateException(
"@ExceptionHandler methods in @ControllerAdvice must be public instance methods");
}
DotName returnTypeDotName = method.returnType().name();
if (DISALLOWED_EXCEPTION_CONTROLLER_RETURN_TYPES.contains(returnTypeDotName)) {
throw new IllegalStateException(
"@ExceptionHandler methods in @ControllerAdvice classes can only have void, ResponseEntity or POJO return types");
}
if (!RESPONSE_ENTITY.equals(returnTypeDotName)) {
reflectiveClassProducer.produce(
ReflectiveClassBuildItem.builder(returnTypeDotName.toString()).methods().fields().build());
}
// we need to generate one JAX-RS ExceptionMapper per Exception type
Type[] handledExceptionTypes = exceptionHandlerInstance.value().asClassArray();
for (Type handledExceptionType : handledExceptionTypes) {
reflectiveClassProducer.produce(View on GitHub (pinned to e1c734241f)
Solutions
- Make the @ExceptionHandler method public and remove the static modifier.
- If it doesn't need Spring request state, move static logic into a helper called from a public instance handler.
- Split unrelated handlers into separate @ControllerAdvice classes if visibility rules conflict.
Example fix
// before
@ExceptionHandler(IllegalArgumentException.class)
static ResponseEntity<String> handle(IllegalArgumentException e) { ... }
// after
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handle(IllegalArgumentException e) { ... } Defensive patterns
Strategy: validation
Validate before calling
// audit advice methods before build
@ControllerAdvice
public class Advice {
@ExceptionHandler(MyEx.class)
public ResponseEntity<?> handle(MyEx e) { ... } // must be public, non-static
} Prevention
- Convention: every @ExceptionHandler method is public and an instance method.
- Never mark handler methods static; use injected fields instead.
When it happens
Trigger: Declaring an @ExceptionHandler method inside a @ControllerAdvice class that is private/protected/package-private, or declared static.
Common situations: Copying Spring code where visibility was relaxed; making a helper handler static because it uses no state; IDE-generated handler methods with default visibility.
Related errors
- Parameter type <type> is being used multiple times in method
- Parameter type <type> is not supported for method<method> of
- Parameter type '<type>' is not supported for method '<method
- You can only have a single class annotated with @ControllerA
- Spring Web can only work if 'quarkus-resteasy-jackson' or 'q
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/1d8e20731d06354c.
Report an issue: GitHub.