quarkusio/quarkus · error · IllegalStateException
For reactive methods running on the event loop, @Transaction
Error message
For reactive methods running on the event loop, @Transactional can only be used if the method returns a `Uni`. Found '" + result.getClass().getName() + "' instead.
What it means
A reactive @Transactional interceptor intercepted a method that runs on the event loop, but the method did not return a Uni, so the interceptor cannot attach transaction steps to the reactive chain. It throws IllegalStateException naming the actual return type found.
Source
Thrown at extensions/reactive-transactions/runtime/src/main/java/io/quarkus/reactive/transaction/runtime/TransactionalInterceptorBase.java:253
// Note: Mutiny wraps checked exceptions in CompletionException, so they appear as RuntimeException here
return actualRollback(connection.transaction(), exception);
});
}
private Uni<Void> actualRollback(Transaction transaction, Throwable exception) {
return toUni(transaction.rollback())
.onFailure().invoke(() -> LOG.tracef("Failed to rollback transaction: %s", transaction))
.invoke(() -> LOG.tracef("Transaction rolled back: %s due to exception %s", transaction, exception));
}
@SuppressWarnings("unchecked")
public static Uni<Object> proceedUni(InvocationContext context) {
try {
Object result = context.proceed();
if (result instanceof Uni<?> uniResult) {
return (Uni<Object>) uniResult;
} else {
throw new IllegalStateException(
"For reactive methods running on the event loop, @Transactional can only be used if the method returns a `Uni`. Found '"
+ result.getClass().getName() + "' instead.");
}
} catch (Exception e) {
return Uni.createFrom().failure(e);
}
}
public static boolean reactiveInterceptorShouldRun() {
boolean condition = Context.isOnEventLoopThread();
LOG.tracef("Transactional interceptor should run: %s", condition);
return condition;
}
protected void validateLegacyPanacheAnnotations() {
// We are running on the retrieved context, however, the method also switch the safety flag.
Context ignored = vertxContext();
if (ContextLocals.get(SESSION_ON_DEMAND_KEY).isPresent()) {View on GitHub (pinned to e1c734241f)
Solutions
- Change the method return type to Uni<T> (or Uni<Void> for void methods).
- If the method is truly blocking, move it off the event loop (@RunOnVirtualThread or worker execution) and use blocking @Transactional.
- Drop the @Transactional annotation if no transaction is required.
Example fix
// before
@Transactional
public CompletionStage<Item> find(Long id) { ... }
// after
@Transactional
public Uni<Item> find(Long id) { ... } Defensive patterns
Strategy: type-guard
Validate before calling
// before calling a @Transactional reactive method
if (method.getReturnType() != Uni.class && !Modifier.isStatic(method.getModifiers()))
throw new IllegalStateException("@Transactional reactive method must return Uni: " + method); Type guard
static boolean returnsUni(java.lang.reflect.Method m) { return Uni.class == m.getReturnType(); } Try / catch
try {
return service.find(id).await().indefinitely();
} catch (IllegalStateException e) {
log.error("@Transactional method must return Uni: {}", e.getMessage());
throw e;
} Prevention
- Make every @Transactional reactive method return Uni (Uni<Void> instead of void)
- Use @ReactiveTransactional for Uni-returning methods
- Keep blocking methods off the event loop and use blocking @Transactional
- Add annotation-processor or ArchUnit checks for return types
When it happens
Trigger: Annotating a @Transactional (non-@ReactiveTransactional path) method running on the event loop whose return type is CompletionStage, Uni-based wrapper other than Uni, void, or a plain object instead of Uni<Object>.
Common situations: Adding @Transactional to a Panache repository method returning CompletionStage; forgetting @ReactiveTransactional semantics and returning a raw entity; copying blocking @Transactional usage into a reactive service.
Related errors
- Unhandled async return type
- Calling a method annotated with @Transactional from a method
- Calling a method annotated with @Transactional from a method
- Calling a method annotated with @Transactional from a method
- No current Vertx context found
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/305d943a618667d0.
Report an issue: GitHub.