The tenancyId null guard from last time left agentId exposed. findById takes two required parameters — I only protected one of them.

Crash or silence: what null means without a guard

Without Objects.requireNonNull, a null agentId behaves differently depending on which registry backend is active. InMemoryAgentRegistry uses a ConcurrentHashMap, which throws NullPointerException immediately — but with no message, which makes the stack trace roughly as helpful as the error itself. JpaAgentRegistry passes null to the JPQL parameter and silently returns empty. No exception, no complaint.

Two backends, two completely different failure modes, neither diagnosable.

The reactive JPA registry was the trickier one to test. The @WithSession CDI interceptor wraps synchronous exceptions into failed Unis rather than letting them propagate directly — I covered this last time. assertThatNullPointerException().isThrownBy(...) doesn’t work here; the NPE surfaces as a Uni failure instead.

UniAsserter has an obvious form: assertFailedWith(supplier, NullPointerException.class). That checks the type but not the message — which matters when two different failure modes both produce NPEs. Claude decompiled quarkus-test-vertx-3.32.2.jar to check whether a better overload existed. There was:

asserter.assertFailedWith(
    () -> registry.findById(null, "default"),
    t -> assertThat(t).isInstanceOf(NullPointerException.class)
                      .hasMessageContaining("agentId"));

The Consumer<Throwable> overload isn’t in the Quarkus documentation — the type-class form is the one every example shows. We found it only by looking at the interface directly.

Javadoc as the authoritative surface

The SPI interfaces — AgentRegistry, ReactiveAgentRegistry — had no @throws documentation on findById. Once the guards exist, the interface is where callers learn about them:

/**
 * @throws NullPointerException if agentId or tenancyId is null
 */
Optional<AgentDescriptor> findById(String agentId, String tenancyId);

The interface is the contract; the Javadoc is what IDEs surface when a caller autocompletes findById. That’s the right place for it — not in a README, not in a comment inside an implementation.

One small housekeeping item: AgentQuery’s compact constructor had been using "tenancyId must not be null" as its message while everything else used the bare field name. I normalized it. Inconsistency in null messages makes a stack trace harder to read at 2am.


<
Previous Post
The hook client, a dead branch, and two bugs found late
>
Next Post
What Was Wrong