quarkusio/quarkus · error · RuntimeException

Failed to substitute

Error message

Failed to substitute ${param}

What it means

When a recorder parameter's type has an ObjectSubstitution registered, BytecodeRecorderImpl instantiates the substitution and calls serialize(); any exception in that path is wrapped as 'Failed to substitute <param>'. The root cause is inside the custom ObjectSubstitution implementation (or reflection on it), not in Quarkus.

Solutions

  1. Look at the 'Caused by' stack trace to find the exception inside your ObjectSubstitution.serialize()
  2. Make sure the substitution class has a public no-arg constructor (it is instantiated reflectively)
  3. Make serialize() handle null/edge-case fields defensively and only capture serializable state
  4. Verify the substitution's serialize/deserialize are symmetrical so the generated bytecode can rebuild the object at runtime
  5. Log the offending param's state before serialize() to spot which field is problematic

Example fix

// before
public Foo serialize(Foo t) { return new Foo(t.inner.name); } // NPE when inner==null
// after
public Foo serialize(Foo t) {
    return new Foo(t.inner == null ? null : t.inner.name);
}
Defensive patterns

Strategy: validation

Validate before calling

// before relying on a substitution
ObjectSubstitution<Foo, FooData> sub = new MyFooSubstitution();
FooData data = sub.serialize(testFoo); // run at build step time to surface failures early
assert sub.deserialize(data) != null : "serialize/deserialize not symmetrical";

Type guard

static <F, T> boolean isSubstitutable(Object param, Class<F> from) {
    return from.isInstance(param);
}

Try / catch

try {
    recorder.accept(foo);
} catch (RuntimeException e) {
    throw new IllegalStateException("Substitution failed for " + foo.getClass(), e.getCause());
}

Prevention

When it happens

Trigger: A recorder method receives an object whose class (or declared expectedType) has a registered ObjectSubstitution, and substitution.serialize(param) throws — e.g. NullPointerException on a null field, missing no-arg constructor on the substitution class, or a checked exception during serialization.

Common situations: Extension authors writing custom ObjectSubstitution classes; substitutions broken after refactoring the substituted type (renamed/removed fields); substitution class lacking a public no-arg constructor.

Related errors


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

Appendix: source

Thrown at core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java:663

                return new DeferredArrayStoreParameter(param, expectedType) {

                    @Override
                    void doPrepare(MethodContext context) {
                        serialized.prepare(context);
                        super.doPrepare(context);
                    }

                    @Override
                    ResultHandle createValue(MethodContext creator, MethodCreator method, ResultHandle array) {
                        ResultHandle subInstance = method.newInstance(MethodDescriptor.ofConstructor(finalHolder.sub));
                        return method.invokeInterfaceMethod(
                                ofMethod(ObjectSubstitution.class, "deserialize", Object.class, Object.class), subInstance,
                                creator.loadDeferred(serialized));
                    }
                };

            } catch (Exception e) {
                throw new RuntimeException("Failed to substitute " + param, e);
            }

        } else if (param instanceof Optional) {
            Optional val = (Optional) param;
            if (val.isPresent()) {
                DeferredParameter res = loadObjectInstance(val.get(), existing, Object.class, relaxedValidation);
                return new DeferredArrayStoreParameter(param, expectedType) {

                    @Override
                    void doPrepare(MethodContext context) {
                        res.prepare(context);
                        super.doPrepare(context);
                    }

                    @Override
                    ResultHandle createValue(MethodContext context, MethodCreator method, ResultHandle array) {
                        // If the value is a proxy, it may be non-null at build time but become null
                        // when we actually create the value during initialization;

View on GitHub (pinned to e1c734241f)