Netflix/Hystrix · error · FallbackDefinitionException

Incompatible return types. \nCommand method: " + commandMeth

Error message

Incompatible return types. \nCommand method: " + commandMethod + ";\nFallback method: " + fallbackMethod + ";\n" + "Hint: " + StringUtils.join(msg, "\n")

What it means

validateReturnType performs deep generic comparison between the command's and fallback's parametrized return types (e.g. Future<User> vs Future<Admin>, Observable<List<String>> vs Observable<List<Integer>>). When the flattened type-argument lists mismatch, each Error gets a reason plus a position hint ('Command type literal pos: ...; Fallback type literal pos: ...' and an '<Ooops!>' marker for the offending parent generic), all joined into createErrorMsg's 'Incompatible return types' FallbackDefinitionException.

Source

Thrown at hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/utils/FallbackMethod.java:180

            List<Type> commandParametrizedTypes = flattenTypeVariables(commandMethod.getGenericReturnType());
            List<Type> fallbackParametrizedTypes = flattenTypeVariables(fallbackMethod.getGenericReturnType());
            Result result = equalsParametrizedTypes(commandParametrizedTypes, fallbackParametrizedTypes);
            if (!result.success) {
                List<String> msg = new ArrayList<String>();
                for (Error error : result.errors) {
                    Optional<Type> parentKindOpt = getParentKind(error.commandType, commandParametrizedTypes);
                    String extraHint = "";
                    if (parentKindOpt.isPresent()) {
                        Type parentKind = parentKindOpt.get();
                        if (isParametrizedType(parentKind)) {
                            extraHint = "--> " + ((ParameterizedType) parentKind).getRawType().toString() + "<Ooops!>\n";
                        }
                    }
                    msg.add(String.format(error.reason + "\n" + extraHint + "Command type literal pos: %s; Fallback type literal pos: %s",
                            positionAsString(error.commandType, commandParametrizedTypes),
                            positionAsString(error.fallbackType, fallbackParametrizedTypes)));
                }
                throw new FallbackDefinitionException(createErrorMsg(commandMethod, method, StringUtils.join(msg, "\n")));
            }
        }
        validatePlainReturnType(commandMethod, fallbackMethod);
    }

    private void validatePlainReturnType(Method commandMethod, Method fallbackMethod) {
        validatePlainReturnType(commandMethod.getReturnType(), fallbackMethod.getReturnType(), commandMethod, fallbackMethod);
    }

    private void validatePlainReturnType(Class<?> commandReturnType, Class<?> fallbackReturnType, Method commandMethod, Method fallbackMethod) {
        if (!commandReturnType.isAssignableFrom(fallbackReturnType)) {
            throw new FallbackDefinitionException(createErrorMsg(commandMethod, fallbackMethod, "Fallback method '"
                    + fallbackMethod + "' must return: " + commandReturnType + " or its subclass"));
        }
    }

    private void validateParametrizedType(Type commandReturnType, Type fallbackReturnType, Method commandMethod, Method fallbackMethod) {
        if (!commandReturnType.equals(fallbackReturnType)) {

View on GitHub (pinned to 5ce3bc58c3)

Solutions

  1. Read the hint: it names the exact type literal positions that differ in both signatures
  2. Make the fallback's generic arguments exactly equal (equals-comparison, not assignability) to the command's, e.g. Observable<User> on both sides
  3. If a different subtype is genuinely needed, change the command's return type to the common supertype first
  4. Add compile-time or CI validation invoking each annotated method once so mismatches surface before production

Example fix

// before
@HystrixCommand
public Observable<User> getUser(String id) { ... }

private Observable<AdminUser> getUserFallback(String id) { ... }

// after
@HystrixCommand
public Observable<User> getUser(String id) { ... }

private Observable<User> getUserFallback(String id) { ... }
Defensive patterns

Strategy: validation

Validate before calling

Type cmdType = commandMethod.getGenericReturnType();
Type fbType = fallbackMethod.getGenericReturnType();
if (!Objects.equals(flatten(cmdType), flatten(fbType))) {
    throw new IllegalStateException("Generic return types differ: " + cmdType + " vs " + fbType);
}
// flatten = recursively collect raw type + type arguments into a List<Type>

Type guard

boolean fallbackTypeMatches(Method command, Method fallback) {
    return Objects.equals(command.getGenericReturnType(), fallback.getGenericReturnType());
}

Try / catch

catch (FallbackDefinitionException e) { log.error("{}", e.getMessage()); /* message contains type-literal positions — align generics exactly and restart */ }

Prevention

When it happens

Trigger: Command returns Observable<User> while fallback returns Observable<UserExt> (type-variable mismatch at a nested literal); command ListenableFuture<List<A>> vs fallback ListenableFuture<List<B>>; generics erased or substituted differently through a generic base class.

Common situations: Fallback copy-pasted from another command with similar DTOs; refactoring DTO hierarchies so a fallback's generic argument is a sibling rather than identical type; generic repository methods where type variables resolve differently for command and fallback.

Related errors


AI-assisted analysis of Netflix/Hystrix@5ce3bc58c3 (2026-08-14). Data as JSON: /api/errors/55ad50b997a51f3a. Report an issue: GitHub.