quarkusio/quarkus · error · UnsupportedOperationException

Calling a method annotated with @Transactional from a method

Error message

Calling a method annotated with @Transactional from a method annotated with @ReactiveTransactional is not supported. Use either @Transactional or @WithSessionOnDemand/@WithSession/@WithTransaction, but not both, throughout your whole application.

What it means

The @Transactional interceptor detects a REACTIVE_TRANSACTIONAL_METHOD_KEY flag, meaning a @ReactiveTransactional method is up the call stack. Mixing the legacy @ReactiveTransactional annotation with plain @Transactional is unsupported and throws UnsupportedOperationException.

Source

Thrown at extensions/reactive-transactions/runtime/src/main/java/io/quarkus/reactive/transaction/runtime/TransactionalInterceptorBase.java:286

    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()) {
            throw new UnsupportedOperationException(
                    "Calling a method annotated with @Transactional from a method annotated with @WithSessionOnDemand is not supported. "
                            + "Use either @Transactional or @WithSessionOnDemand/@WithSession/@WithTransaction, "
                            + "but not both, throughout your whole application.");
        }

        if (ContextLocals.get(WITH_TRANSACTION_METHOD_KEY).isPresent()) {
            throw new UnsupportedOperationException(
                    "Calling a method annotated with @Transactional from a method annotated with @WithTransaction is not supported. "
                            + "Use either @Transactional or @WithSessionOnDemand/@WithSession/@WithTransaction, "
                            + "but not both, throughout your whole application.");
        }

        if (ContextLocals.get(REACTIVE_TRANSACTIONAL_METHOD_KEY).isPresent()) {
            throw new UnsupportedOperationException(
                    "Calling a method annotated with @Transactional from a method annotated with @ReactiveTransactional is not supported. "
                            + "Use either @Transactional or @WithSessionOnDemand/@WithSession/@WithTransaction, "
                            + "but not both, throughout your whole application.");
        }
    }

    /**
     *
     * @return the current vertx duplicated context
     * @throws IllegalStateException If no vertx context is found or is not a safe context as mandated by the
     *         {@link VertxContextSafetyToggle}
     */
    private static Context vertxContext() {
        Context context = Vertx.currentContext();
        if (context != null) {
            VertxContextSafetyToggle.validateContextIfExists(ERROR_MSG, ERROR_MSG);
            return context;
        } else {

View on GitHub (pinned to e1c734241f)

Solutions

  1. Use @Transactional consistently on both methods (the reactive interceptor handles Uni-returning methods).
  2. Alternatively keep @ReactiveTransactional on both, but never nest one inside the other.
  3. Add an architecture test to enforce a single transactional annotation style.

Example fix

// before
@ReactiveTransactional
public Uni<Item> outer() { return inner(); }
@Transactional
public Uni<Item> inner() { ... }
// after
@Transactional
public Uni<Item> outer() { return inner(); }
@Transactional
public Uni<Item> inner() { ... }
Defensive patterns

Strategy: validation

Validate before calling

void check(Class<?> bean) {
    for (Method m : bean.getDeclaredMethods())
        if (m.isAnnotationPresent(ReactiveTransactional.class) && callsTransactionalMethods(bean, m))
            throw new IllegalStateException("@ReactiveTransactional wrapping @Transactional: " + m);
}

Try / catch

try {
    return service.outer();
} catch (UnsupportedOperationException e) {
    log.error("Do not nest @ReactiveTransactional with @Transactional: {}", e.getMessage());
    throw e;
}

Prevention

When it happens

Trigger: Calling a @Transactional-annotated method from a method annotated with @ReactiveTransactional (the two interceptors cannot safely nest).

Common situations: Code migrated partially from @ReactiveTransactional to @Transactional; developers assuming both are equivalent and combining them.

Related errors


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