Troubleshooting
Symptom → likely cause → fix. If your app isn't behaving as expected, find the symptom here first.
Rendering
The render is blank / empty.
Likely the app hash isn't registered, or you're calling the wrong render router. Check ComponentRegistry.exists(hash)
returns true, and that your deployment's router address is correct.
A data value shows "unavailable."
The bound resolver reverted, is missing, ran out of gas, or doesn't support the requested kind. This is fail-closed
working correctly — the app refused to fabricate a value. Check the resolver address, that it supportsKind your
kind, and that its own read isn't reverting.
A slot always shows its fallback text, even though the resolver and kind are right.
The arg is probably encoded the wrong way. Each kind decodes its args in one exact shape — abi.encode of the
listed args, or raw bytes when the kind is marked packed. ensName, for example, takes exactly the 20 address
bytes; a 32-byte abi.encode(address) or a namehash reverts "bad args" and the slot falls back. Check the kind on
All Resolver Kinds and use the SDK encoder where one exists
(kinds.ens.encodeNameArgs).
A role, direction, variant or other setting I wrote isn't in the render.
compile() silently drops a value that isn't in the dictionary (role: "note", direction: "vertical") and any
field it doesn't read — the tree still compiles. Run checkNode(tree) (SDK 0.10.0+, or the MCP
chainrender_check_node tool): it lists each dropped field with its path. Allowed values are on
Node Fields.
u8 out of range: undefined when compiling.
A node has a type the compiler doesn't have — most often "heading". A heading is { "type": "text", "role": "heading" } (what compose.heading() emits). checkNode reports an unknown type before you compile.
A chart renders as axes only (no data).
The chart's data source is unavailable — same fail-closed behavior. Verify the IChartData source resolves.
The output looks different across RPCs. It shouldn't — rendering is deterministic. If output genuinely differs, you're likely rendering different app hashes (e.g. a stale hash on one side) or hitting different router versions. Confirm the same hash + router.
Grade
My grade dropped from A to C unexpectedly.
You introduced a live/off-chain dependency. Read the grade's breakdown — it names the exact deviation. Common
causes: an external image (CLIENT_FETCH), a mutable dependency, or a fetch without an on-chain fallback. Swap to
an on-chain resolver to return to A.
The enforcer refused my app.
Check which rail: over/under-declared capabilities (R2), an action outside the envelope (R3), a value transfer
without VALUE (R4), an arbitrary {to,selector} (R5), or an unanalyzable token (I5). The rejection message names
it. Declare precisely what you reach; never add an arbitrary-call path. → The Capability Enforcer
Theming
My theme change didn't appear.
Two cases: (1) if you changed tokens but the deployed app renders an old seeded theme, you need to re-seed and
point the app at the new theme id; (2) if you're using the presentation context, confirm you're calling
renderComponent with the new themeId, not the plain renderComponent.
A spacing/size change had no visible effect.
The value you changed may be hardcoded rather than token-driven, or you scaled a token that the visible elements
don't reference. Confirm the element uses var(--cr-*). (This exact class of issue is why the token system exists.)
My theme was rejected at build. It failed an invariant — most likely a font below 12px (legibility floor) or a foreground/background pair below WCAG AA (contrast invariant). Fix the token, don't bypass the check.
Actions
The wallet transaction does the wrong thing / nothing.
Confirm the action declaration (to, sig, args) matches your contract's actual function. describeAction
prints the human-readable call — check it says what you expect before signing. buildActionRequest constructs
exactly the declared call, so a wrong result means a wrong declaration.
Deployment
MetaMask says a chunk transaction is "likely to fail".
After Glamsterdam, storing a large chunk can need more gas than the wallet will send. The visual builder sizes chunks
from the chain's txGasCap and measured storageGas in chains.json (Sepolia: 45,000,000 gas, about 23.6 KB per
chunk) and refuses to send a transaction above the cap. If you still see the warning, the builder you're using
predates that change, or the chain has no txGasCap entry. → Size Limits & Glamsterdam
A Hardhat script fails with "fields had validation errors". Hardhat's local signer refuses any transaction above 30,000,000 gas. Keep the transaction under it, or sign it with viem directly. → Local Build Environment
npm audit reports a high-severity Zod advisory.
It's GHSA-mrhx-c7xc-m3w5, reached through Hardhat 3's dependency on Zod 3, for which no patch exists. It's dev-only.
Don't run npm audit fix --force (it downgrades to Hardhat 2) or override Zod to 4 (it breaks Hardhat's config
validation). → Local Build Environment
My app hash changed and I didn't expect it. You changed something baked into identity — text, structure, a role, a declared action, or a baked argument (like a chart's dimensions or a resolver address in a builder). Presentation (theme/palette) is render-time and doesn't change the hash; everything else does. → Content Addressing
A viewer/client shows stale behavior after I updated something on-chain. The client may be hand-holding a copy of an ABI or address that's now out of date. Ensure your client reads its ABI and addresses from a single source (the compiled artifact / the addresses file), not a hand-maintained copy — stale duplicated references are a common integration pitfall.
Still stuck?
- Re-read the render as bytes: decode the actual
renderHashoutput and look at what's really there, rather than assuming. Most "impossible" render bugs are visible in one look at the real output. - Check the Gotchas, Do's & Don'ts — your issue may be a known pattern.