The v0.9.0 placeholder was an empty const: export const procedureKinds = {} as const satisfies ProcedureKinds. Read it and you got undefined. v0.12.0 replaced it with a throwing Proxy. The change is a shift in failure philosophy (from fail-open to fail-closed) and this post is the reasoning behind it.
The failure mode of fail-open
In the old design, when a bundler failed to resolve the alias and the adapter read kinds[procedure], it got undefined. The adapter's hook-selection code treated undefined as "show both hooks". Nothing crashed. Nothing logged. The app ran.
That is the seduction and the poison of fail-open. On the surface everything works: both hooks render, calls go out. But every procedure silently gets both useQuery and useMutation, the exact API you designed the kinds system to prevent. And the type-level inference is quietly wrong too, because the placeholder's ProcedureKinds = Record<string, never> gives every key an undefined kind. The wrongness is distributed across the whole app, invisible in any single call site.
The cost of the silent path
A query/mutation mismatch is not a cosmetic issue. Calling a mutation through useQuery means the client sends a POST as a GET-cached query, the mutation may run twice, or be deduplicated away by TanStack Query's cache keyed on the input. The hook shape you use determines request semantics. Exposing the wrong one when codegen never ran is not a graceful degradation; it is a live bug wearing a mask.
Fail-closed: the throw is the feature
The Proxy design flips the default. If the generated module is not resolved, reading kinds throws with a message that names the exact fix. The failure is:
- Early, at module load, not at a random interaction on page 3.
- Loud, a red stack trace instead of a greyed-out hook.
- Actionable, the error tells you to run
pyrpc devand verify the alias. - Local, it points at the exact mechanism that broke (resolution), not at a symptom.
The tradeoff, stated honestly
Fail-closed has a cost: a genuinely unconfigured project now refuses to run, where before it limped along. But for a developer setting up pyRPC, a crash with a fix-it message is a better experience than a mysterious cache bug three days later. And the window where you hit the throw is tiny, codegen configures the tsconfig and bundler alias automatically, so the throw is the canary for "your tooling and my aliases disagreed".
The general rule
Placeholders are for types, not for behavior. A missing value that changes request semantics is a hard failure by design. v0.12.0's placeholder is the line in the sand: if the type machinery is not in place, you will know (loudly) instead of shipping hooks that were never meant to exist.

pyRPC