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

  1. Change the method return type to Uni<T> (or Uni<Void> for void methods).
  2. If the method is truly blocking, move it off the event loop (@RunOnVirtualThread or worker execution) and use blocking @Transactional.
  3. 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

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


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/305d943a618667d0. Report an issue: GitHub.