quarkusio/quarkus · error · CacheException
Unhandled async return type
Error message
Unhandled async return type
What it means
asyncInvocationResultToUni converts an intercepted method's result into a Uni for caching. It supports Uni and CompletionStage return types; if it is invoked with any other ReturnType (i.e. NonAsync slipped through), it throws CacheException wrapping an IllegalStateException. This normally indicates an internal dispatch bug rather than user error.
Source
Thrown at extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheInterceptor.java:245
}
if (CompletionStage.class.isAssignableFrom(returnType)) {
return ReturnType.CompletionStage;
}
return ReturnType.NonAsync;
}
protected Uni<?> asyncInvocationResultToUni(Object invocationResult, ReturnType returnType) {
if (returnType == ReturnType.Uni) {
return (Uni<?>) invocationResult;
} else if (returnType == ReturnType.CompletionStage) {
return Uni.createFrom().completionStage(new Supplier<>() {
@Override
public CompletionStage<?> get() {
return (CompletionStage<?>) invocationResult;
}
});
} else {
throw new CacheException(new IllegalStateException(UNHANDLED_ASYNC_RETURN_TYPE_MSG));
}
}
protected Object createAsyncResult(Uni<Object> cacheValue, ReturnType returnType) {
if (returnType == ReturnType.Uni) {
return cacheValue;
}
if (returnType == ReturnType.CompletionStage) {
return cacheValue.subscribeAsCompletionStage();
}
throw new CacheException(new IllegalStateException(UNHANDLED_ASYNC_RETURN_TYPE_MSG));
}
protected enum ReturnType {
NonAsync,
Uni,
CompletionStage
}View on GitHub (pinned to e1c734241f)
Solutions
- Verify the cached method returns exactly Uni or CompletionStage (or a plain, non-async type) and nothing exotic.
- If you subclass CacheInterceptor, ensure determineReturnType and asyncInvocationResultToUni stay consistent.
- If hit without custom code, report it as a Quarkus bug with the method signature and version.
Example fix
// before
@CacheResult(cacheName = "x")
public SomeReactiveType get(String id) { ... } // unsupported async type
// after
@CacheResult(cacheName = "x")
public Uni<SomeType> get(String id) { ... } Defensive patterns
Strategy: validation
Validate before calling
Class<?> rt = cachedMethod.getReturnType();
if (!io.smallrye.mutiny.Uni.class.isAssignableFrom(rt)
&& !java.util.concurrent.CompletionStage.class.isAssignableFrom(rt)
&& isAsyncDispatch(rt)) {
throw new IllegalStateException("Return Uni or CompletionStage for async caching");
} Prevention
- Restrict cached methods to Uni, CompletionStage, or plain return types
- Avoid exotic reactive wrappers (Mono, Maybe) on @CacheResult methods
- Keep interceptor subclasses consistent with determineReturnType
When it happens
Trigger: A cached method whose declared return type was classified as NonAsync by determineReturnType but still routed through the async conversion path — practically only reachable via subclassing CacheInterceptor or inconsistent classification (e.g. exotic return types like Uni subtypes handled inconsistently across custom interceptor subclasses).
Common situations: Custom interceptor subclasses overriding determineReturnType or the invocation flow; framework-level regressions after upgrading Quarkus; Kotlin suspend/CompletableFuture variants misclassified.
Related errors
- Unknown item type: ${otherItem.getClass()}
- Unknown item type: ${otherItem.getClass()}
- Multiple listeners returned discovery root elements for: ${d
- No listeners returned a discovery root element
- Could not find registered EnumDefinition for ${name}
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/72a4cfc445c7a22f.
Report an issue: GitHub.