{"record":{"id":"4d59e17d10dd642d","repo":"Netflix/Hystrix","slug":"fallback-cannot-return-future-if-the-fallback-isn","errorCode":null,"errorMessage":"fallback cannot return Future if the fallback isn't command when the command is async.","messagePattern":"fallback cannot return Future if the fallback isn't command when the command is async\\.","errorType":"exception","errorClass":"FallbackDefinitionException","httpStatus":null,"severity":"error","filePath":"hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/utils/FallbackMethod.java","lineNumber":129,"sourceCode":"                    validateParametrizedType(commandParametrizedType, method.getGenericReturnType(), commandMethod, method);\n                } else {\n                    validateReturnType(commandMethod, method);\n                }\n\n\n            } else if (ExecutionType.ASYNCHRONOUS == ExecutionType.getExecutionType(commandReturnType)) {\n                if (isCommand() && ExecutionType.ASYNCHRONOUS == getExecutionType()) {\n                    validateReturnType(commandMethod, method);\n                }\n                if (ExecutionType.ASYNCHRONOUS != getExecutionType()) {\n                    Type commandParametrizedType = commandMethod.getGenericReturnType();\n                    if (isReturnTypeParametrized(commandMethod)) {\n                        commandParametrizedType = getFirstParametrizedType(commandMethod);\n                    }\n                    validateParametrizedType(commandParametrizedType, method.getGenericReturnType(), commandMethod, method);\n                }\n                if (!isCommand() && ExecutionType.ASYNCHRONOUS == getExecutionType()) {\n                    throw new FallbackDefinitionException(createErrorMsg(commandMethod, method, \"fallback cannot return Future if the fallback isn't command when the command is async.\"));\n                }\n            } else {\n                if (ExecutionType.ASYNCHRONOUS == getExecutionType()) {\n                    throw new FallbackDefinitionException(createErrorMsg(commandMethod, method, \"fallback cannot return Future if command isn't asynchronous.\"));\n                }\n                if (ExecutionType.OBSERVABLE == getExecutionType()) {\n                    throw new FallbackDefinitionException(createErrorMsg(commandMethod, method, \"fallback cannot return Observable if command isn't observable.\"));\n                }\n                validateReturnType(commandMethod, method);\n            }\n\n        }\n    }\n\n    private Type getFirstParametrizedType(Method m) {\n        Type gtype = m.getGenericReturnType();\n        if (gtype instanceof ParameterizedType) {\n            ParameterizedType pType = (ParameterizedType) gtype;","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/Netflix/Hystrix/blob/5ce3bc58c38e7ca60ef2fe0e516e390e294ad941/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/utils/FallbackMethod.java#L111-L147","documentation":"FallbackMethod.validate() checks that a fallback method is compatible with its @HystrixCommand method. When the command's execution type is ASYNCHRONOUS (returns Future/CompletableFuture/ListenableFuture), a fallback that also returns Future but is NOT itself a command (no @HystrixCommand on the fallback, i.e. not a chained async fallback) is rejected with FallbackDefinitionException, because Javanica cannot wrap a plain Future-returning fallback into the async command pipeline.","triggerScenarios":"@HystrixCommand method returning Future<User> with a fallback method `Future<User> getUserFallback(Throwable t)` that has no @HystrixCommand annotation of its own.","commonSituations":"Copying a synchronous fallback signature while converting the command to async; assuming Future-returning fallbacks 'just work' like synchronous ones; missing the requirement that async-over-async fallback chaining requires the fallback itself to be a Hystrix command.","solutions":["Annotate the fallback method with @HystrixCommand so it becomes a command and legal async chaining works","Or change the fallback to return the plain value (User) while the command returns Future — Javanica lifts it","Or implement the fallback inside HystrixCommand.getFallback() by switching from annotation-style to explicit command class","Re-run the validation at startup with a test that invokes each async command once"],"exampleFix":"// before\n@HystrixCommand\npublic Future<User> getUser(String id) { ... }\n\nprivate Future<User> getUserFallback(String id) { ... }\n\n// after\n@HystrixCommand\npublic Future<User> getUser(String id) { ... }\n\n@HystrixCommand\nprivate Future<User> getUserFallback(String id) { ... }","handlingStrategy":"validation","validationCode":"Class<?> cmdRet = commandMethod.getReturnType();\nClass<?> fbRet = fallbackMethod.getReturnType();\nboolean cmdAsync = Future.class.isAssignableFrom(cmdRet);\nif (cmdAsync && Future.class.isAssignableFrom(fbRet)\n        && fallbackMethod.getAnnotation(HystrixCommand.class) == null) {\n    throw new IllegalStateException(\"Async fallback returning Future must itself be @HystrixCommand\");\n}","typeGuard":"null // no language-level guard; Javanica validates reflectively — encode the rule in a startup check as above","tryCatchPattern":"catch (FallbackDefinitionException e) { log.error(\"Fallback signature invalid: {}\", e.getMessage()); /* fix signature and restart */ }","preventionTips":["Follow the Javanica fallback matrix: async command -> plain value OR @HystrixCommand-annotated async fallback","Smoke-test every command/fallback pair in CI","Keep fallback signatures generated/checked by a shared base class or ArchUnit rule"],"tags":["hystrix","javanica","fallback","async","future","validation"],"backgroundTag":null,"analyzedSha":"5ce3bc58c38e7ca60ef2fe0e516e390e294ad941","analyzedAt":"2026-08-14T10:55:35.600Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}