Open-Source Wikis

/

Solid

/

How to contribute

/

Debugging

solidjs/solid

Debugging

Active contributors: Ryan Carniato, Damian Tarnawski

This page collects techniques for understanding what the runtime is doing — both inside this repo and inside an app that consumes Solid.

Use the dev build

packages/solid/package.json declares a development export condition:

"development": {
  "types": "./types/index.d.ts",
  "import": "./dist/dev.js",
  "require": "./dist/dev.cjs"
}

The dev bundle (dist/dev.js) is built by packages/solid/rollup.config.js with replaceDev(true), which inlines _SOLID_DEV_ as true. That toggle (declared in packages/solid/src/reactive/signal.ts as IS_DEV) gates:

  • The DEV hooks object exposed from solid-js:
    export const DEV = IS_DEV
      ? ({ hooks: DevHooks, writeSignal, registerGraph } as const)
      : undefined;
  • Source map bookkeeping (SourceMapValue, the name parameter on createSignal / createMemo, the Owner.sourceMap array).
  • Multi-instance warnings (globalThis.Solid$$ deduplication check at the bottom of packages/solid/src/index.ts).
  • Friendly error messages — most throw new Error(...) calls in the runtime have a long-form dev message and a short prod string.

In a Vite or Rollup app, set the development export condition (Vite does this automatically for dev servers). If you are exercising the source directly via the Vitest config, IS_DEV is wired through the alias resolution.

DevHooks

packages/solid/src/reactive/signal.ts exports a mutable DevHooks object:

export const DevHooks: {
  afterUpdate: (() => void) | null;
  afterCreateOwner: ((owner: Owner) => void) | null;
  afterCreateSignal: ((signal: SignalState<any>) => void) | null; // @deprecated
  afterRegisterGraph: ((sourceMapValue: SourceMapValue) => void) | null;
};

Solid's official devtools attach to these hooks at startup. You can do the same in a debugging session:

import { DEV } from 'solid-js';
DEV?.hooks.afterCreateOwner = (owner) => console.log('new owner', owner.name);
DEV?.hooks.afterRegisterGraph = (n) => console.log('graph', n.name, n.value);

packages/solid/store/src/store.ts has its own DevHooks.onStoreNodeUpdate that fires when a store property changes.

Naming nodes for diagnostics

Most reactive primitives accept a name option in the dev build:

const [count, setCount] = createSignal(0, { name: 'count' });
const doubled = createMemo(() => count() * 2, undefined, { name: 'doubled' });

The name flows into the SourceMapValue and is exposed via DevHooks.afterRegisterGraph to devtools.

Common debugging recipes

"Why isn't my effect re-running?"

The reactive graph wires subscriptions at read time. If the signal is being read inside an untrack(...), inside a non-reactive context, or via destructuring in a way that turns an accessor into a value at module load, the subscription was never created. getListener() (exported from solid-js) returns the active Computation if you are inside a tracking scope and null otherwise — useful for an inline console.log(getListener()?.name) to verify you are tracked.

"Why is my effect running too often?"

Use createMemo to memoize the upstream computation, or pass { equals: ... } to a signal/memo to widen the equality check. equalFn (the default) is Object.is. Setting equals: false forces every set to notify.

"Suspense never resolves"

<Suspense> (in packages/solid/src/render/Suspense.ts) tracks pending resources via a context-supplied increment/decrement pair. Resources increment on read() while pending and decrement on resolution. Common causes:

  • A resource is created in a parent of the <Suspense> boundary and so the boundary never sees the increment.
  • read() is called inside untrack, breaking subscription.
  • A resource fetcher rejected without an onError boundary; the error short-circuits resolution. <ErrorBoundary> is your friend.

"SSR output is empty / errors with is not supported in the browser"

The browser bundle of solid-js/web ships server-mock.ts (packages/solid/web/src/server-mock.ts) which logs an error and returns undefined for renderToString, renderToStringAsync, etc. If you see those errors, you are calling SSR APIs from a browser-condition build. Switch the export condition to node/deno/worker (or use the default non-browser entry) to get the real implementation in packages/solid/src/server/.

"Hydration mismatch / duplicated DOM nodes"

The CHANGELOG notes 1.8 made hydration more robust against streamed-in lazy components and top-level fragments. If you still see hydration corruption:

  1. Confirm the SSR and client builds were produced with the same babel-preset-solid version (this is the very first item in the CHANGELOG 1.8 "Other" section: a Babel/runtime version mismatch breaks hydration markers).
  2. Inspect sharedConfig (packages/solid/src/render/hydration.ts) — its context.id and context.count should match between the SSR HTML attributes and the client.
  3. Check for non-deterministic component output (random keys, Date.now(), etc.) that diverges between server and client.

Debugging the build itself

If a Rollup build is producing an unexpected shape:

cd packages/solid
pnpm build:js  # runs `rollup -c` only

packages/solid/rollup.config.js is the single source of truth for entry points and external mappings. The replaceDev helper at the top of that file is the place to add a new dev-only marker.

Debugging types

pnpm --filter solid-js test-types runs tsc --project tsconfig.test.json against the *.type-tests.ts suite. To narrow in on a single error:

cd packages/solid
npx tsc --project tsconfig.test.json --noEmit | head -50

The first error is usually the meaningful one; everything below tends to be cascade failures.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Debugging – Solid wiki | Factory