quarkusio/quarkus · error · RuntimeException

%s is marked @Record but does not inject an @Recorder object

Error message

%s is marked @Record but does not inject an @Recorder object

What it means

Quarkus build steps marked with @Record must inject at least one @Recorder object, because @Record marks a step as bytecode-recorded (its logic runs at RUNTIME_INIT via generated recorders). During extension loading, Quarkus scans the step's parameters for a Recorder type; if none is found the extension definition is invalid and loading fails fast.

Source

Thrown at core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java:453

            if (!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
                method.setAccessible(true);
            }
            final Class<? extends BooleanSupplier>[] onlyIf = buildStep.onlyIf();
            final Class<? extends BooleanSupplier>[] onlyIfNot = buildStep.onlyIfNot();
            final Parameter[] methodParameters = method.getParameters();
            final Record recordAnnotation = method.getAnnotation(Record.class);
            final boolean isRecorder = recordAnnotation != null;
            final boolean identityComparison = !isRecorder || recordAnnotation.useIdentityComparisonForParameters();
            if (isRecorder) {
                boolean recorderFound = false;
                for (Class<?> p : method.getParameterTypes()) {
                    if (isRecorder(p)) {
                        recorderFound = true;
                        break;
                    }
                }
                if (!recorderFound) {
                    throw new RuntimeException(method + " is marked @Record but does not inject an @Recorder object");
                }
            }
            final List<BiFunction<BuildContext, BytecodeRecorderImpl, Object>> methodParamFns;
            Consumer<BuildStepBuilder> methodStepConfig = Functions.discardingConsumer();
            BooleanSupplier addStep = () -> true;
            addStep = and(addStep, supplierFactory, classOnlyIf, false);
            addStep = and(addStep, supplierFactory, classOnlyIfNot, true);
            addStep = and(addStep, supplierFactory, onlyIf, false);
            addStep = and(addStep, supplierFactory, onlyIfNot, true);
            final BooleanSupplier finalAddStep = addStep;

            if (isRecorder) {
                final ExecutionTime executionTime = recordAnnotation.value();
                final boolean optional = recordAnnotation.optional();
                methodStepConfig = methodStepConfig.andThen(bsb -> {
                    bsb
                            .produces(
                                    executionTime == ExecutionTime.STATIC_INIT ? StaticBytecodeRecorderBuildItem.class

View on GitHub (pinned to e1c734241f)

Solutions

  1. Add a parameter of a class annotated with @Recorder to the recorded build step and use it to record runtime logic.
  2. Remove the @Record annotation if the step does not actually need to record bytecode for runtime initialization.
  3. Split the step: keep pure build-time logic in an unrecorded step and move recorder usage into a separate @Record step.

Example fix

// before
@BuildStep
@Record(RUNTIME_INIT)
void recordSomething(MyBuildItem item) { ... }
// after
@BuildStep
@Record(RUNTIME_INIT)
void recordSomething(MyBuildItem item, MyRecorder recorder) {
    recorder.doAtRuntime(item.getValue());
}
Defensive patterns

Strategy: validation

Validate before calling

boolean hasRecorder = Arrays.stream(buildStepMethod.getParameterTypes())
    .anyMatch(t -> t.isAnnotationPresent(Recorder.class) || Recorder.class.isAssignableFrom(t));
if (!hasRecorder) throw new IllegalStateException("@Record step " + buildStepMethod + " lacks an @Recorder parameter");

Type guard

static boolean isRecordedStepWithRecorder(Method m) {
    return m.isAnnotationPresent(Record.class)
        && Arrays.stream(m.getParameterTypes()).anyMatch(t -> t.isAnnotationPresent(Recorder.class));
}

Prevention

When it happens

Trigger: An extension author annotates a @BuildStep method with @Record(RUNTIME_INIT) (or EXECUTION) but all its parameters are non-recorder types (build items, config, etc.), so isRecorder(p) is false for every parameter during loadStepsFromClass.

Common situations: Refactoring a recorded step and removing the Recorder parameter; copy-pasting a @Record annotation onto a step that only consumes build items; misunderstanding that @Record applies to build-time bytecode generation only.

Related errors


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