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 @WithSessionOnDemand is not supported. Use either @Transactional or @WithSessionOnDemand/@WithSession/@WithTransaction, but not both, throughout your whole application.

What it means

The reactive @Transactional interceptor detects a leftover @WithSessionOnDemand marker in the Vertx context locals, meaning a legacy Panache session annotation is active up the call stack. Mixing legacy Panache session annotations with plain @Transactional is unsupported, so it throws UnsupportedOperationException.

Source

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

                        "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()) {
            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.");
        }

View on GitHub (pinned to e1c734241f)

Solutions

  1. Remove @WithSessionOnDemand from the calling method and rely on @Transactional (or vice versa).
  2. Standardize the whole application on @Transactional/@ReactiveTransactional for transactional reactive methods.
  3. Grep the codebase for @WithSessionOnDemand/@WithSession/@WithTransaction and remove overlaps with @Transactional.

Example fix

// before
@WithSessionOnDemand
public Uni<Item> load() { return service.find(); }
@Transactional public Uni<Item> find() { ... }
// after
@Transactional
public Uni<Item> load() { return service.find(); }
@Transactional
public Uni<Item> find() { ... }
Defensive patterns

Strategy: validation

Validate before calling

// detect mixed transactional annotations before runtime
void checkAnnotations(Class<?> bean) {
    for (Method m : bean.getDeclaredMethods()) {
        boolean legacy = m.isAnnotationPresent(WithSessionOnDemand.class);
        boolean tx = m.isAnnotationPresent(Transactional.class);
        if (legacy && tx) throw new IllegalStateException("Mixed @WithSessionOnDemand + @Transactional in " + bean);
    }
}

Try / catch

try {
    return txService.find(id);
} catch (UnsupportedOperationException e) {
    log.error("Incompatible tx annotations in call chain: {}", e.getMessage());
    throw e;
}

Prevention

When it happens

Trigger: Calling a method annotated @Transactional from a method annotated @WithSessionOnDemand (the flag SESSION_ON_DEMAND_KEY is present in the context).

Common situations: Old Panache code using @WithSession/@WithSessionOnDemand gradually migrated to @Transactional, leaving mixed annotations in one call chain.

Related errors


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