quarkusio/quarkus · error · IllegalStateException

The @Blocking, @NonBlocking and @RunOnVirtualThread annotati

Error message

The @Blocking, @NonBlocking and @RunOnVirtualThread annotations may only be used on "entrypoint" methods (methods invoked by various frameworks in Quarkus)
Using the @Blocking, @NonBlocking and @RunOnVirtualThread annotations on methods that can only be invoked by application code is invalid

What it means

Quarkus's execution-model annotation processor verifies that @Blocking, @NonBlocking and @RunOnVirtualThread appear only on entrypoint methods (those invoked by frameworks: CDI observers, HTTP endpoints, scheduled tasks, etc.). If it finds them on methods only callable from application code, it logs a failure message; in strict mode it throws IllegalStateException.

Source

Thrown at core/deployment/src/main/java/io/quarkus/deployment/execannotations/ExecutionModelAnnotationsProcessor.java:53

        if (config.detectionMode() == ExecutionModelAnnotationsConfig.Mode.DISABLED) {
            return;
        }

        StringBuilder message = new StringBuilder("\n");
        doCheck(message, index.getIndex(), predicates, BLOCKING);
        doCheck(message, index.getIndex(), predicates, NON_BLOCKING);
        doCheck(message, index.getIndex(), predicates, RUN_ON_VIRTUAL_THREAD);

        if (message.length() > 1) {
            message.append("The @Blocking, @NonBlocking and @RunOnVirtualThread annotations may only be used "
                    + "on \"entrypoint\" methods (methods invoked by various frameworks in Quarkus)\n");
            message.append("Using the @Blocking, @NonBlocking and @RunOnVirtualThread annotations on methods "
                    + "that can only be invoked by application code is invalid");
            if (config.detectionMode() == ExecutionModelAnnotationsConfig.Mode.WARN) {
                log.warn(message);
            } else {
                throw new IllegalStateException(message.toString());
            }
        }
    }

    private void doCheck(StringBuilder message, IndexView index,
            List<ExecutionModelAnnotationsAllowedBuildItem> predicates, DotName annotationName) {

        List<String> badMethods = new ArrayList<>();
        for (AnnotationInstance annotation : index.getAnnotations(annotationName)) {
            // these annotations may be put on classes too, but we'll ignore that for now
            if (annotation.target() != null && annotation.target().kind() == AnnotationTarget.Kind.METHOD) {
                MethodInfo method = annotation.target().asMethod();
                boolean allowed = false;
                for (ExecutionModelAnnotationsAllowedBuildItem predicate : predicates) {
                    if (predicate.matches(method)) {
                        allowed = true;
                        break;
                    }

View on GitHub (pinned to e1c734241f)

Solutions

  1. Move the annotation to the actual entrypoint (resource method, @Scheduled method, observer) and let the execution model propagate to helpers via context
  2. Remove the annotation from non-entrypoint methods — it has no effect there anyway
  3. Set quarkus.execution-model-detection-mode=warn temporarily to downgrade to a warning while migrating
  4. Split logic: keep an entrypoint method annotated, call unannotated internal methods from it

Example fix

// before
@RunOnVirtualThread
void helper() { ... } // only called from application code
// after
void helper() { ... } // annotate only the entrypoint, e.g. the JAX-RS method or @Scheduled method
Defensive patterns

Strategy: validation

Validate before calling

// Build-time check habit: annotate only methods that Quarkus invokes itself
// Entry points: JAX-RS resource methods, @Scheduled, CDI observers (@Observes), messaging consumers
if (!isQuarkusEntryPoint(method)) throw new IllegalStateException("@Blocking/@NonBlocking/@RunOnVirtualThread only on entrypoints: " + method);

Type guard

static boolean isExecutionModelAnnotation(AnnotationInstance a) {
    return a.name().equals(DotName.createSimple("io.smallrye.common.annotation.Blocking"))
        || a.name().equals(DotName.createSimple("io.smallrye.common.annotation.NonBlocking"))
        || a.name().equals(DotName.createSimple("io.smallrye.common.annotation.RunOnVirtualThread"));
}

Try / catch

try {
    quarkusBuild();
} catch (IllegalStateException e) {
    if (e.getMessage().contains("entrypoint\" methods")) {
        // locate the annotated non-entrypoint method named in the message and remove/move the annotation
    }
    throw e;
}

Prevention

When it happens

Trigger: quarkus-execution-model-annotation build step 'check' during augmentation finds the annotations on a private/internal method or a method invoked solely by other application code, with quarkus.execution-model-detection-mode=strict (default).

Common situations: Developer annotates a helper method with @Blocking or @RunOnVirtualThread intending to influence the caller; annotation inherited onto internal methods via a class-level/inheritable placement mistake; refactor moves an annotated entrypoint method into internal-only code.

Related errors


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