quarkusio/quarkus · error · IllegalStateException

Currently suspend methods for Reactive Messaging are not sup

Error message

Currently suspend methods for Reactive Messaging are not supported on methods that are only annotated with @Outgoing

What it means

Kotlin suspend functions are supported as Reactive Messaging mediators, but not when the method is only @Outgoing (a publisher). The invoker generation step throws IllegalStateException because the TODO'd publisher-side coroutine support is unimplemented. @Incoming (or incoming+outgoing) suspend methods are fine.

Source

Thrown at extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/SmallRyeReactiveMessagingProcessor.java:443

            baseName = DotNames.simpleName(bean.getImplClazz().enclosingClass()) + "_"
                    + DotNames.simpleName(bean.getImplClazz().name());
        } else {
            baseName = DotNames.simpleName(bean.getImplClazz().name());
        }
        StringBuilder sigBuilder = new StringBuilder();
        sigBuilder.append(method.name()).append("_").append(method.returnType().name().toString());
        for (Type i : method.parameterTypes()) {
            sigBuilder.append(i.name().toString());
        }
        String targetPackage = DotNames.internalPackageNameWithTrailingSlash(bean.getImplClazz().name());
        String generatedName = targetPackage + baseName
                + INVOKER_SUFFIX + "_" + method.name() + "_"
                + HashUtil.sha1(sigBuilder.toString());

        if (isSuspendMethod
                && ((mediatorConfiguration.getIncoming().isEmpty()) && (mediatorConfiguration.getOutgoing() != null))) {
            // TODO: this restriction needs to be lifted
            throw new IllegalStateException(
                    "Currently suspend methods for Reactive Messaging are not supported on methods that are only annotated with @Outgoing");
        }

        if (!isSuspendMethod) {
            generateStandardInvoker(method, gizmo, generatedName);
        } else if (!mediatorConfiguration.getIncoming().isEmpty()) {
            generateSubscribingCoroutineInvoker(method, gizmo, generatedName);
        }

        return generatedName.replace('/', '.');
    }

    private void generateStandardInvoker(MethodInfo method, Gizmo gizmo, String generatedName) {
        String dotName = generatedName.replace('/', '.');
        gizmo.class_(dotName, cc -> {
            cc.implements_(Invoker.class);

            String beanInstanceType = method.declaringClass().name().toString();

View on GitHub (pinned to e1c734241f)

Solutions

  1. Remove `suspend` from the @Outgoing-only method and return a reactive type (Multi/Uni/Publisher) instead.
  2. Do the async work inside a coroutineScope launched from a non-suspend method, returning the resulting Multi/Uni.
  3. Convert the method to include an @Incoming stream so suspend is supported.

Example fix

// before
@Outgoing("out")
suspend fun produce(): String = compute()
// after
@Outgoing("out")
fun produce(): Uni<String> = Uni.createFrom().item { runBlocking { compute() } }
Defensive patterns

Strategy: validation

Validate before calling

// Ensure suspend methods always have an @Incoming:
fun isValidSuspendMediator(hasIncoming: Boolean, hasOutgoing: Boolean, isSuspend: Boolean): Boolean =
    !isSuspend || hasIncoming

Prevention

When it happens

Trigger: Declaring `suspend fun` combined with only @Outgoing; the deployment then generates the invoker and hits this check in generatedInvokerName/generateInvoker.

Common situations: Kotlin users writing periodic producers with @Outgoing as suspend functions after seeing suspend+@Incoming examples work.

Related errors


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