{"record":{"id":"08162a1b5045c7c1","repo":"spring-projects/spring-framework","slug":"only-one-afterthrowing-method-per-specific-throwab","errorCode":null,"errorMessage":"Only one afterThrowing method per specific Throwable subclass allowed: {} / {}","messagePattern":"Only one afterThrowing method per specific Throwable subclass allowed: (.+?) / (.+?)","errorType":"exception","errorClass":"AopConfigException","httpStatus":null,"severity":"error","filePath":"spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptor.java","lineNumber":109,"sourceCode":"\t\t\t\t}\n\t\t\t\telse if (method.getParameterCount() == 4) {\n\t\t\t\t\t// Method, Object[], target, throwable\n\t\t\t\t\tClass<?>[] paramTypes = method.getParameterTypes();\n\t\t\t\t\tif (!Method.class.equals(paramTypes[0]) || !Object[].class.equals(paramTypes[1]) ||\n\t\t\t\t\t\t\tThrowable.class.equals(paramTypes[2]) || !Throwable.class.isAssignableFrom(paramTypes[3])) {\n\t\t\t\t\t\tthrow new AopConfigException(\"Invalid afterThrowing signature: \" +\n\t\t\t\t\t\t\t\t\"four arguments must be Method, Object[], target, throwable: \" + method);\n\t\t\t\t\t}\n\t\t\t\t\tthrowableParam = paramTypes[3];\n\t\t\t\t}\n\t\t\t\tif (throwableParam == null) {\n\t\t\t\t\tthrow new AopConfigException(\"Unsupported afterThrowing signature: single throwable argument \" +\n\t\t\t\t\t\t\t\"or four arguments Method, Object[], target, throwable expected: \" + method);\n\t\t\t\t}\n\t\t\t\t// An exception handler to register...\n\t\t\t\tMethod existingMethod = this.exceptionHandlerMap.put(throwableParam, method);\n\t\t\t\tif (existingMethod != null) {\n\t\t\t\t\tthrow new AopConfigException(\"Only one afterThrowing method per specific Throwable subclass \" +\n\t\t\t\t\t\t\t\"allowed: \" + method + \" / \" + existingMethod);\n\t\t\t\t}\n\t\t\t\tif (logger.isDebugEnabled()) {\n\t\t\t\t\tlogger.debug(\"Found exception handler method on throws advice: \" + method);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (this.exceptionHandlerMap.isEmpty()) {\n\t\t\tthrow new AopConfigException(\n\t\t\t\t\t\"At least one handler method must be found in class [\" + throwsAdvice.getClass() + \"]\");\n\t\t}\n\t}\n\n\n\t/**\n\t * Return the number of handler methods in this advice.\n\t */","sourceCodeStart":91,"sourceCodeEnd":127,"githubUrl":"https://github.com/spring-projects/spring-framework/blob/69bf83ad716d0cfc4b0520a19b4d8b24c79d1538/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptor.java#L91-L127","documentation":"Thrown as AopConfigException by ThrowsAdviceInterceptor's constructor when two afterThrowing methods on the same advice are keyed to the same Throwable subclass. The exceptionHandlerMap (keyed by throwable parameter type) detected an existing entry at line 107-110, so the second registration is rejected to keep dispatch unambiguous.","triggerScenarios":"A ThrowsAdvice class declares two methods like 'afterThrowing(IllegalStateException ex)' and 'afterThrowing(Method, Object[], Object, IllegalStateException ex)'; both register under IllegalStateException and the second triggers the error.","commonSituations":"Adding the four-arg overload alongside the one-arg overload for the same exception type; merging two advice implementations that handle the same exception; refactoring that left a duplicate handler.","solutions":["Keep only one handler per Throwable type: pick either the one-arg or the four-arg form for a given exception class.","If you need both context-rich and simple handling, choose the four-arg form and read context from its parameters.","Register different methods for different exception subclasses (one handler per class is allowed)."],"exampleFix":"// before\npublic void afterThrowing(IllegalStateException ex) { ... }\npublic void afterThrowing(Method m, Object[] a, Object t, IllegalStateException ex) { ... } // duplicate\n\n// after\npublic void afterThrowing(Method m, Object[] a, Object t, IllegalStateException ex) { ... }\npublic void afterThrowing(Method m, Object[] a, Object t, IllegalArgumentException ex) { ... } // distinct type","handlingStrategy":"validation","validationCode":"Map<Class<?>, Method> seen = new HashMap<>();\nfor (Method m : advice.getClass().getMethods()) {\n  if (!m.getName().equals(\"afterThrowing\")) continue;\n  Class<?> exType = (m.getParameterCount() == 1) ? m.getParameterTypes()[0] : m.getParameterTypes()[3];\n  if (seen.put(exType, m) != null) {\n    throw new IllegalStateException(\"Duplicate afterThrowing for \" + exType + \": \" + m);\n  }\n}","typeGuard":"static boolean uniqueThrowablesPerMethod(Method[] ms) {\n  Set<Class<?>> seen = new HashSet<>();\n  for (Method m : ms) {\n    if (!m.getName().equals(\"afterThrowing\")) continue;\n    Class<?> t = (m.getParameterCount() == 1) ? m.getParameterTypes()[0] : m.getParameterTypes()[3];\n    if (!seen.add(t)) return false;\n  }\n  return true;\n}","tryCatchPattern":"try { new ThrowsAdviceInterceptor(advice); }\ncatch (AopConfigException e) {\n  if (e.getMessage().contains(\"Only one afterThrowing method per specific Throwable\")) { /* keep one handler per type */ }\n  throw e;\n}","preventionTips":["Provide at most one afterThrowing handler per Throwable subclass.","Use the four-arg form when you also need the one-arg behavior; do not declare both for the same type.","Distinct exception subclasses can each have their own handler."],"tags":["spring-aop","throws-advice","configuration","validation","duplicate"],"backgroundTag":null,"analyzedSha":"69bf83ad716d0cfc4b0520a19b4d8b24c79d1538","analyzedAt":"2026-08-09T15:32:58.770Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}