---
title: "Error Codes and explain() Reference"
description: "Every LemonadeJS error carries a stable LJS-xxx code with the cause and the fix in the message, pattern-matchable by tools and agents, plus explain()."
source: https://lemonadejs.com/docs/errors/
---

# Error codes and explain()

Every LemonadeJS error and warning carries a **stable code**, `LJS-xxx`,
with a one-line cause and a one-line fix in the message itself, designed
to be pattern-matched by tools and agents. `explain(code)` returns the
long-form documentation offline:

```javascript
import { explain } from 'lemonadejs';
explain('LJS-302');
// "The bind directive needs the state object itself: bind="${name}"
//  (not bind="name", which is a string, and not bind="${name.value}",
//  which is a one-time snapshot). Create it with const name = state(""). "
```

`explain()` returns the long-form documentation of any code, offline, in the dev build.

<!--example-->

```js
import { html, explain } from 'lemonadejs';

const codes = ['LJS-003', 'LJS-104', 'LJS-201', 'LJS-202', 'LJS-301', 'LJS-302', 'LJS-303', 'LJS-305'];

const App = (props, { state }) => {
    const code = state('LJS-302');
    return html`<div>
        <select bind="${code}">${() => codes.map((c) => html`<option value="${c}">${c}</option>`)}</select>
        <pre style="white-space:pre-wrap">${() => explain(code.value)}</pre>
    </div>`;
};
```

## The problem

Debugging is hypothesis-driven from error strings. Three diagnostic
patterns recur in the field, in escalating cost:

- **Warnings as prose.** Prose warnings are human-readable but not stable
  across versions, not greppable as a stable identity, and carry no
  machine-checkable code a tool can branch on.
- **Errors behind a URL.** Error codes pointing to an external decoder put
  the message somewhere else; the long-form explanation may require a
  browser and a network connection.
- **Errors without messages, the most expensive class.** Some mistake
  classes produce no runtime diagnostic, just a silent wrong value. The
  author reads the code, sees nothing wrong, and iterates blindly. A
  thrown `LJS-302` with the fix in the message is repaired in one turn; a
  silent wrong value can consume an entire debugging session.

## How LemonadeJS solves it

**One code per mistake, fix included.** Errors (`fail`) throw with their
code in dev *and* production; the code is the identity, stable across
versions. Warnings (`warn`) are dev-only tripwires for mistakes that
would otherwise be silent: a snapshot that looks like it was meant to be
live (`LJS-202`), a miscased callback that a component would silently
ignore (`LJS-305`), a `value` attribute fighting `bind` (`LJS-304`).
The deliberate exception is documented as a code too: in-place mutation
without `touch()` is silent by design, and `LJS-201` names the price of
big-data mutation freedom.

**`explain()` is offline documentation.** Development builds carry the
full long-form text for every code; the entire table is declared behind
the dev flag (`!DEV ? {} : {...}`), so production builds **dead-code-
eliminate it completely**: zero bytes shipped, nothing to configure.
A production `explain()` still answers: the short message plus a pointer
to the dev build and `llms.txt`. Which mode you get is decided by the
artifact you load: `dist/lemonade.dev.js` and the bundler `development`
exports condition give checks and full texts; `dist/lemonade.min.js` and
production bundles are stripped.

**Codes gate publication.** `verify(Component)` fails on *any* engine
warning, so a block that triggers `LJS-202` in some prop combination
cannot enter the Studio registry. A warning is not advice here; it is a
failing check with a name. See [Tests](/docs/tests/).

## The table

Twenty-one codes, from `src/errors.ts`. This is the complete set:

| Code | Meaning |
|---|---|
| LJS-001 | Component is not a function |
| LJS-002 | Component must return a template created with `` html`...` `` |
| LJS-003 | `mount()` requires a DOM element as root |
| LJS-101 | Unexpected closing tag; check tag nesting |
| LJS-102 | Unclosed tag at the end of the template |
| LJS-104 | Unknown component; `setComponents({ Card })`, or embed by value: `<${Card} />` |
| LJS-105 | Expression `${...}` is not allowed in this position |
| LJS-201 | In-place mutation is silent; call `state.touch()` after mutating, or assign |
| LJS-202 | Slot holds a snapshot; wrap dynamic expressions: `${() => ...}` |
| LJS-203 | Update loop detected; a state change keeps triggering itself |
| LJS-204 | Duplicate key in a list; keys must be unique for identity matching |
| LJS-205 | A template expression threw; contained, other updates continued |
| LJS-206 | `resource` fetcher reads its own data/loading/error; async update loop |
| LJS-301 | Event attributes require a function: `onclick="${() => ...}"` |
| LJS-302 | bind requires a state: `bind="${state}"` |
| LJS-303 | bind works on `<input>`, `<textarea>` and `<select>`; on components it is a prop |
| LJS-304 | bind owns the element value; remove the explicit value/checked attribute |
| LJS-305 | Event and callback names are lowercase: `onclick`, `onchange`, `onsave` |
| LJS-401 | Prop does not match its contract |
| LJS-402 | Unknown prop, not declared in the contract (with a did-you-mean hint) |
| LJS-501 | Sugar singletons: expose once, never touch; check the api in the contract |

`LJS-205` names a behavior, not just a mistake: the failing expression is
contained: its DOM keeps the last good content while every other binding
in the pass still runs. The full containment story, and `trace()`, are in
[Debugging](/docs/debugging/).

## Codes drive the engine

A stable code does more than help the session where it fires: it makes a
mistake *class* countable. When the same code keeps appearing while real
components are built, the engine absorbs the class instead of documenting
a workaround. The Studio catalog left receipts:

- **`LJS-203` → `subscribe()` runs untracked.** The gantt block bumped a
  `version` counter from a subscription; `version.value++` *reads* the
  state it writes, which inside a tracked context subscribed the callback
  to itself, an update loop. The fix was not a note in the docs: the
  engine now runs every `subscribe()` callback untracked, pinned to its
  one source state. The very line that exposed the bug ships today,
  protected by the contract it forced (`components/gantt`):

  ```javascript
  const refresh = () => {
      computeRange();
      preview.value = null;
      // Safe since subscribe() runs callbacks untracked (the engine
      // guard born from this very line; it used to LJS-203 loop)
      version.value++;
  };
  ```

- **The same class via refs and setup.** Ref callbacks and component
  setup bodies used to execute inside the enclosing binding's tracked
  context, creating phantom subscriptions that surfaced as `LJS-203`
  loops. The engine now wraps both in `untracked()`. Whole class removed.
- **Renderer-caused `focusout`.** Three blocks independently shipped the
  same bug (an editor committing on blur, a dropdown closing mid-open, a
  dock losing its click) when the renderer removed a focused element. The
  answer became an engine primitive, `isDisposing()`, used by seven
  Studio blocks today (see [Destroy](/docs/destroy/)).
- **Accidental silent mutation.** Dev builds freeze state contents so an
  unintended mutation throws `LJS-201` instead of doing nothing.

## In practice

The loop an agent actually runs, with a real code:

```javascript
// 1. generated code
html`<input bind="${name.value}" />`
// 2. mount throws: "LJS-302: bind requires a state: bind="${state}",
//    got string in <input>"
// 3. the message IS the fix:
html`<input bind="${name}" />`
```

One turn, no source-diving, no hypothesis tree. The same loop works on
warnings because `verify()` converts them into failing checks: a block
that warns anywhere in its prop matrix fails its proof before it reaches
the registry.

## Reference

```javascript
import { explain } from 'lemonadejs';

explain(code)   // long-form documentation, offline (dev build)
                // prod: short message + pointer; unknown code → says so
```

- Errors throw with their code in every build; warnings are dev-only.
- Dev/prod is the **artifact**, not a flag: `lemonade.dev.js` /
  `development` exports condition vs `lemonade.min.js` / production
  bundles. Production pays zero for any of this.
- The complete code table also ships in `llms.txt`: one request instead
  of eighteen lookups.