Error codes

Every diagnostic What can raise, all 31 of them: 25 errors and 6 warnings.

Each code carries a suggestion and a worked example. The same catalogue backs the what_errors MCP tool, so an agent debugging your app reads exactly what you read here.

All codes

ERR_ACTION_FAILED

The server action rejected.

error Raised by what-server.

The server action rejected. The message is the one the action threw, forwarded to the client by the action handler. Throw a typed error from the action and branch on its shape rather than parsing this string.

// In the action, fail with something the client can read:
export const save = action(async (data) => {
  if (!data.email) throw Object.assign(new Error('Email required'), { field: 'email' });
});

ERR_CHILDREN_ONLY

React.Children.only expected to receive a single React element child.

error Raised by what-react.

Children.only asserts exactly one element. Pass one child, or use Children.toArray/Children.map when the count can vary.

// Bad:
<Tooltip><span>a</span><span>b</span></Tooltip>

// Good:
<Tooltip><span>a</span></Tooltip>

ERR_DESTRUCTURED_PROPS

Destructuring 'binding' in the component body snapshots props and loses reactivity.

warning Raised by what-devtools-mcp.

What components run ONCE, so the body is not re-run when a prop changes. Reading `props.foo` goes through the reactive proxy and tracks; `const { foo } = props` reads the value once and detaches it. Read through the proxy inside JSX and effects, or wrap each field in an accessor.

// Bad - snapshots at first run, never updates:
function Row(props) {
  const { label } = props;
  return <span>{label}</span>;
}

// Good - reads through the proxy each time:
function Row(props) {
  return <span>{props.label}</span>;
}

// Good - an accessor keeps the destructured name:
function Row(props) {
  const label = () => props.label;
  return <span>{label()}</span>;
}

ERR_DUPLICATE_ACTION_ID

Duplicate server action ID "id".

error Raised by what-compiler.

Action ids are the wire address of a server action, so two actions cannot share one. Ids derive from the file path and export name, so this usually means the same action is being registered twice, or an explicit id was reused.

// Bad — two actions pinned to the same id:
export const save = action(fn, { id: 'save' });
export const store = action(fn, { id: 'save' });

// Good — let ids derive, or make them distinct:
export const save = action(fn);
export const store = action(fn, { id: 'store-user' });

ERR_FORM_ACTION_MISSING

[what] <Form> requires an `action` prop: a server action or its id.

error Raised by what-server.

Pass the action itself, or the string id it was registered under.

// Bad:
<Form method="post" />

// Good:
<Form action={save} />
<Form action="save-user" />

ERR_FORM_ACTION_NOT_REGISTERED

[what] <Form action={fn}>: that function is not a server action.

error Raised by what-server.

Wrap it with action() from what-server, or pass the action id as a string. A plain function has no id, so there is nothing for the form post to address.

// Bad — a plain function:
async function save(data) {}
<Form action={save} />

// Good — a registered action:
export const save = action(async (data) => {});
<Form action={save} />

ERR_HOOK_OUTSIDE_RENDER

[what-react] hookName() called outside of a component render.

error Raised by what-react.

Hooks can only be called while a what-react component is rendering. When this happens inside a React library, the usual cause is two module instances: make sure every `react` and `react-dom` import is aliased to what-react by the reactCompat() vite plugin.

// vite.config.js
import { reactCompat } from 'what-react/vite';
export default { plugins: [reactCompat()] };

ERR_HYDRATION_MISMATCH

Hydration mismatch in component "component": server rendered "serverHTML" but client expects "clientHTML".

error Raised by what-devtools-mcp.

Ensure server and client render identical initial HTML. Avoid reading browser-only APIs (window, localStorage) during the initial render. Use onMount() for client-only logic.

// Bad — different on server vs client:
function App() {
  return <p>{window.innerWidth}</p>;
}

// Good — use onMount for client-only values:
function App() {
  const width = signal(0);
  onMount(() => width(window.innerWidth));
  return <p>{width()}</p>;
}

ERR_INFINITE_EFFECT

Effect "effectName" exceeded 25 flush iterations — likely an infinite loop.

error Raised by what-core, what-devtools-mcp.

An effect is writing to a signal it also reads, creating a cycle. Use untrack() to read the signal without subscribing, or restructure so the write and read are in separate effects.

// Bad — reads and writes count, creating a cycle:
effect(() => { count(count() + 1); });

// Good — use untrack() so the read doesn't subscribe:
effect(() => { count(untrack(count) + 1); });

// Better — split into separate logic:
const doubled = computed(() => count() * 2);

ERR_INVALID_HTML_NESTING

<parent> cannot contain <child>: the HTML parser closed the outer tag early, so the rendered tree does not match your JSX.

error Raised by what-core.

The compiler turns each element into an HTML template string, and the browser parses it under real HTML rules. Some nestings are not expressible: <p> may only hold phrasing content, <a> may not hold another <a>, and a table section may not hold arbitrary elements. The parser silently reorders them, which leaves compiled output walking a tree that no longer matches your source. Change the outer tag (usually <p> to <div>) or move the child out.

// Bad — the parser closes <p> before <div>, producing four sibling nodes:
<p>Intro<div>{body()}</div>Outro</p>

// Good — a block container can hold block content:
<div class="prose">Intro<div>{body()}</div>Outro</div>

// Good — or keep the paragraph and use phrasing content:
<p>Intro<span>{body()}</span>Outro</p>

ERR_INVALID_SSR_TAG

[what-server] Invalid tag name in SSR: tag.

error Raised by what-server.

renderToString reached a vnode whose tag is neither a string nor a component function. This is almost always a component that returned a raw object, or a value interpolated where an element was expected.

// Bad — returns a plain object, not a vnode:
function Row() { return { name: 'a' }; }

// Good — return elements, and interpolate values as children:
function Row({ name }) { return <li>{name}</li>; }

ERR_ISLAND_STORE_OUTSIDE_RENDER

[what-server] Island store "name" was accessed outside an active server render.

error Raised by what-server.

A module-scoped island store resolves against the current request, so it can only be read or written from a component rendered by renderDocument/renderPage. Reading one at module scope, or from a background task, has no request to bind to.

// Bad — runs at import time, with no request in scope:
const count = cart.items.length;

// Good — read it inside a component the server is rendering:
function Cart() { return <span>{cart.items.length}</span>; }

ERR_ISR_MISSING_CLIENT

[what-isr] createRedisStore requires { client }.

error Raised by what-isr.

what-isr ships no Redis driver on purpose, so the client is injected. Pass an ioredis or node-redis instance (get/set/del/sadd/srem/smembers, optionally expire/scan/keys).

import Redis from 'ioredis';
const store = createRedisStore({ client: new Redis(process.env.REDIS_URL) });

ERR_ISR_VARY_NO_HEADERS

[what-isr] route declares `vary` but the adapter supplied no request headers; refusing to cache.

error Raised by what-isr.

The route varies its output per header, and the adapter called the engine without them. Caching anyway would serve one variant to every request. Forward the request headers from the adapter into the engine call.

// In the adapter:
await engine.handle(routeMatch, { headers: request.headers });

ERR_ISR_VARY_UNRESOLVED

[what-isr] cannot build a cache key: `vary` is declared but could not be resolved against the request.

error Raised by what-isr.

A declared vary is a list of names that must be resolved against real request headers before it can be part of a key. Either pass the request headers alongside the declaration, or pass an already-resolved name -> value object. Guessing would cache one visitor page under another visitor key.

// Bad — a declaration with nothing to resolve it against:
cacheKey({ path, vary: ['cookie:session'] });

// Good — supply the headers:
cacheKey({ path, vary: ['cookie:session'], headers: request.headers });

// Good — or resolve it yourself:
cacheKey({ path, vary: { 'cookie:session': sessionId } });

ERR_MISSING_CLEANUP

Effect sets up "resource" but does not return a cleanup function.

warning Raised by what-devtools-mcp.

Effects that add event listeners, set timers, or open connections should return a cleanup function to prevent memory leaks.

// Bad — no cleanup:
effect(() => {
  window.addEventListener('resize', handler);
});

// Good — return cleanup:
effect(() => {
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
});

ERR_MISSING_KEY

List rendered without key prop in component "component". Items may re-order incorrectly.

warning Raised by what-compiler, what-devtools-mcp.

Add a unique key prop to each item in a list. Use a stable identifier (like an ID), not the array index.

// Bad — no key:
<For each={items()}>{item => <li>{item.name}</li>}</For>

// Good — stable key:
<For each={items()}>{item => <li key={item.id}>{item.name}</li>}</For>

ERR_MISSING_SIGNAL_READ

Signal "signalName" used without calling () — renders as "[Function]" instead of its value.

warning Raised by what-devtools-mcp.

Signals are functions. Call them to read: count() not count. In JSX: {count()} not {count}.

// Bad — signal reference, not value:
<span>{count}</span>       // renders "[Function]"

// Good — call the signal:
<span>{count()}</span>     // renders the actual value

ERR_NO_SECURE_RANDOM

[what] No secure random source available for CSRF token generation.

error Raised by what-server.

Neither globalThis.crypto.getRandomValues nor node:crypto was reachable. On Node this means a build older than 18 or a bundler that stripped node:crypto; on an edge runtime it means the Web Crypto global was not provided. A CSRF token from Math.random is not a token, so this refuses rather than degrading.

// Node 18+ exposes Web Crypto globally; nothing to configure.
// If a bundler dropped it, restore the global before creating the server:
import { webcrypto } from 'node:crypto';
globalThis.crypto ??= webcrypto;

ERR_ORPHAN_EFFECT

Effect "effectName" was created outside a reactive root — it will never be cleaned up.

warning Raised by what-core, what-devtools-mcp.

Wrap effect creation in createRoot() or create effects inside component functions where they are automatically tracked.

// Bad — orphaned, leaks memory:
effect(() => console.log(count()));

// Good — inside a root with cleanup:
createRoot(dispose => {
  effect(() => console.log(count()));
  // later: dispose() cleans up
});

ERR_PAGE_NO_DEFAULT_EXPORT

Page module has no default-exported component.

error Raised by what-framework-cli.

A page file must default-export the component to render. A named export cannot be found by the file-router.

// Bad:
export function Home() { return <h1>Hi</h1>; }

// Good:
export default function Home() { return <h1>Hi</h1>; }

ERR_PRETEXT_NOT_INSTALLED

[what-text] Failed to load @chenglou/pretext: message.

error Raised by what-text.

what-text declares pretext as an optional peer so the package installs without it. Install it to use the text engine: npm install @chenglou/pretext

npm install @chenglou/pretext

ERR_REDIRECT_NOT_CAUGHT

A redirect() to "target" surfaced uncaught, so nothing performed the navigation.

error Raised by what-devtools-mcp, what-router.

redirect() is caught in route middleware and in a component body. From an event handler, a promise callback, a timer or a reactive thunk, call navigate(to) instead. If the call is inside a try/catch, rethrow anything whose name is RouterRedirect. On the server this signal escapes renderToString to its caller: read its `to` and emit a 302 rather than calling navigate().

// Bad - a reactive thunk re-runs outside the render the Router caught:
<div>{() => (loggedOut() ? redirect('/login') : <Dashboard />)}</div>

// Good - navigate() from a thunk or a handler:
<div>{() => (loggedOut() ? (navigate('/login'), null) : <Dashboard />)}</div>
<button onclick={() => navigate('/login')}>Sign in</button>

// On the server, catch it instead of navigating:
try { html = renderToString(<App />); }
catch (e) { if (e.name === 'RouterRedirect') return Response.redirect(e.to, 302); throw e; }

ERR_RUNTIME

An error surfaced that the framework could not classify, so the message is the original one verbatim.

error Raised by what-devtools-mcp.

An error surfaced that the framework could not classify, so the message is the original one verbatim. The stack, file and line on the error narrow it; if the same shape shows up repeatedly it is worth its own code here.

// Inspect the structured form rather than the string:
try { render(); } catch (e) { console.log(classifyError(e).toJSON()); }

ERR_SIGNAL_WRITE_IN_RENDER

Signal "signalName" written during render of component "component". This triggers re-execution.

error Raised by what-devtools-mcp.

Move signal writes into event handlers, effects, or onMount(). The component body should only read signals, not write them.

// Bad — write during render:
function Counter() {
  count(count() + 1);  // triggers infinite loop
  return <span>{count()}</span>;
}

// Good — write in event handler:
function Counter() {
  return <button onclick={() => count(c => c + 1)}>{count()}</button>;
}

ERR_STATIC_WRITE_ESCAPE

[what-server] Refusing to write outside outDir: path.

error Raised by what-server.

A route path resolved to a location outside the export directory, which a "../" segment in a route or a param can do. Sanitize the route path, or drop the route from the static export.

// Bad — a param that can contain a slash escapes outDir:
{ path: '/docs/:slug*', mode: 'static' }

// Good — constrain the param, or precompute the exact paths:
{ path: '/docs/:slug', mode: 'static', paths: () => slugs.map(slug => ({ slug })) }

ERR_UNKNOWN

createWhatError() was called with a code that is not in this catalogue.

error

createWhatError() was called with a code that is not in this catalogue. Check the spelling against ERROR_CODES, or add the entry.

// Bad - not a catalogue key:
createWhatError('MISSING_KEYS');

// Good:
createWhatError('MISSING_KEY', { component: 'TodoList' });

ERR_UNKNOWN_TOOL

The MCP client called a tool this server does not expose.

error Raised by what-mcp.

The MCP client called a tool this server does not expose. Call tools/list to enumerate what is available; a stale client cache is the usual cause.

// List what the server actually exposes:
{ "method": "tools/list" }

ERR_UNSAFE_INNERHTML

innerHTML set on element without using the __html safety marker.

warning Raised by what-devtools-mcp.

Use the html tagged template literal or pass { __html: content } to mark innerHTML as intentional and reviewed.

// Bad — raw innerHTML (XSS risk):
<div innerHTML={userInput} />

// Good — explicit opt-in:
<div innerHTML={{ __html: sanitizedContent }} />

// Better — use the html template literal:
html`<div>${sanitizedContent}</div>`

ERR_UNSAFE_REDIRECT

redirect() refused an unsafe target: target.

error Raised by what-devtools-mcp, what-router.

redirect() accepts same-origin paths and http:, https:, mailto: or tel: URLs only. Protocol-relative ("//host"), backslash-smuggled and javascript:/data: targets are open-redirect vectors. Check a user-supplied target against an allowlist first.

// Bad - a user-controlled target can leave your origin:
redirect(query.next);

// Good - allowlist the target first:
redirect(ALLOWED.has(query.next) ? query.next : '/');

ERR_USE_INVALID_ARG

[what-react] use() expects a promise or a context.

error Raised by what-react.

use() reads either a thenable or a context object. Anything else has nothing to suspend on or subscribe to.

// Good:
const value = use(ThemeContext);
const data = use(fetchUser(id));