---
title: "Debugging Components With trace()"
description: "Debugging with trace(): reactive causality as plain JSON, and LJS-205 containment so one failing expression cannot take down the update pass."
source: https://lemonadejs.com/docs/debugging/
---

# Debugging with trace()

The debugging loop for "my update did not happen" is three lines of
`trace()`: arm, act, read:

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

trace(50);              // arm: record the last 50 events
count.value++;          // act
trace();                // read, plain JSON:
// [{ at: 0, kind: 'write', state: 'counter.s0', old: 0, value: 1 },
//  { at: 1, kind: 'run',   binding: 'counter#slot', cause: 'counter.s0' }]
trace(false);           // disarm and clear
```

Every event names what happened, to which state or binding, caused by
what. Causality stops being something you infer from symptoms and
becomes data you read.

Arm `trace()`, act, read: a mutation without `touch()` records nothing, which is the whole diagnosis.

<!--example-->

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

const App = (props, { state }) => {
    const count = state(0);
    const rows = state([1, 2, 3]);
    const out = state('press a button');

    const run = (act) => {
        trace(20);                                      // arm
        act();                                          // act
        const events = trace();                         // read
        trace(false);                                   // disarm
        out.value = JSON.stringify(events, null, 1);
    };

    return html`<div>
        <p>count: ${count} · rows: ${() => rows.value.length}</p>
        <button onclick="${() => run(() => count.value++)}">count.value++</button>
        <button onclick="${() => run(() => rows.value.push(rows.value.length + 1))}">rows.value.push() only</button>
        <button onclick="${() => run(() => rows.touch())}">rows.touch()</button>
        <pre style="max-height:220px;overflow:auto">${out}</pre>
    </div>`;
};
```

## The problem

Reactive bugs are causality bugs: *something* wrote, *something* ran (or
did not), and the connection between them is invisible in the source.
The established answers each have limitations:

- **Visual devtools assume a human at a browser.** Browser-extension
  devtools (component trees, profiler flame charts, highlight-on-render)
  are rich and visual. Their output is a screen, which a program cannot
  read.
- **Error containment requires application wiring.** The common pattern
  for containing render errors is an opt-in boundary placed by the author:
  a placement decision and a fallback UI required at each site.
- **`console.log` archaeology is the fallback**, for humans and agents
  alike: instrument, re-run, read, repeat. Each iteration costs a run,
  and for an agent each run costs a turn.

## How LemonadeJS solves it

**`trace()`, causality as data.** A ring buffer of the last *n*
reactive events, returned as plain JSON. Five kinds:

| kind | fields | meaning |
|---|---|---|
| `write` | `state`, `old`, `value`, `by` | a state assignment; `by` names the binding that wrote, if any |
| `touch` | `state` | a `touch()` notification |
| `run` | `binding`, `cause` | a binding re-ran; `cause` names the state that triggered it |
| `warn` | `code`, `detail` | an engine warning (`LJS-xxx`) landed in the stream |
| `error` | `binding`, `detail` | a contained `LJS-205` exception, with the failing binding |

Labels are readable, stable identities: `component.s0` /
`component.propname` / `component.computed1` / `store.key` for states;
`component#slot` / `component#attr` / `component#bind:tag` for bindings.
And values in events are **summaries, never references**:
`'Array(5)'`, `'Object'`, strings truncated, so tracing never retains
your data and never alters object lifetimes while you debug memory.

Tracing is **dev-only by build**: production builds return `[]` from
`trace()` and ship none of the machinery (see [Errors](/docs/errors/) for
the dev/prod artifact split).

**`LJS-205`, containment, in dev *and* production.** An exception
inside one reactive expression (a `${() => ...}` slot, an attribute
binding, a computed, a subscribe callback) is contained:

- the failing binding is skipped and its DOM region **keeps its last
  good content**;
- every other binding in the update pass still runs; siblings update;
- the error is logged **once until the expression recovers**; a later
  clean run re-arms logging, so a fixed bug that regresses logs again;
- with tracing armed, the error lands in the buffer with its binding
  label.

The boundary is per-*binding*, not per-component, and it requires no
placement decision: the unit of containment is the unit of update.
Containment keeps the last good DOM; it does not render a designed
fallback UI; that remains the application's concern.

One deliberate exception: **engine diagnostics are never contained.**
An `LJS-xxx` failure like the `LJS-203` loop guard propagates. A
broken program should fail loudly, only a broken *expression* should
fail locally.

**`inspect()` and `explain()` close the loop.** `inspect(element)`
returns the live component tree for any element (names, state values,
children) as plain JSON; the [test harness](/docs/tests/) exposes the
same view as `t.inspect()`. `explain('LJS-205')` prints the long-form
documentation for any code, offline, in the same process.

## In practice

**"My update did not happen"**: the two most common verdicts, read
straight from the buffer:

```javascript
trace(50);
rows.value.push(newRow);     // mutation only...
trace();                     // → []. NO write event: nothing notified.
                             //   The LJS-201 case: add rows.touch()

trace(50);
rows.touch();
trace();
// [{ kind: 'touch', state: 'grid.s0' }]            notification happened
// ...but no { kind: 'run', cause: 'grid.s0' }      nothing subscribed:
// the template read rows WITHOUT a live expression (the LJS-202 case)
```

The absence of an event is as diagnostic as its presence, and absences
are exactly what log-based debugging cannot show.

**A contained failure, observed.** From the engine suite: a slot reads
`user.value.name`, and `user` becomes `null`. The slot throws: `<b>`
keeps showing the last good name, the sibling `<i>` binding still
updates to "out", `LJS-205` is logged once, and with tracing armed the
buffer holds `{ kind: 'error', binding: '...#slot', detail: '...' }`.
Repeated failures stay silent until a clean run re-arms logging, so the
console shows state *changes*, not a scroll of identical stack traces.

**The agent loop.** `verify()` already turns warnings into failing
checks at publication time ([Contracts](/docs/contracts/)); `trace()` is
the same philosophy at debug time: the engine's behavior serialized
into the form a model consumes natively. An agent reproducing a bug
arms the buffer, performs the action, and pattern-matches the JSON it
gets back.

## Reference

```javascript
import { trace, inspect, explain } from 'lemonadejs';

trace(n)        // arm: ring buffer of the last n events (dev builds)
trace()         // read: TraceEvent[], plain JSON, summarized values
trace(false)    // disarm and clear
                // production builds: always []

// TraceEvent kinds:
// write { state, old, value, by? }   touch { state }
// run   { binding, cause }           warn  { code, detail }
// error { binding, detail }          contained LJS-205 exceptions

inspect(el)     // live component tree for any element: names, state
                // values, children: plain JSON
explain(code)   // long-form doc for any LJS code, offline (dev build)
```

- Containment (`LJS-205`): per binding, dev **and** production; last
  good DOM kept; siblings run; log-once-until-recovery.
- Engine diagnostics (`LJS-xxx` failures, e.g. `LJS-203`) are never
  contained; they propagate.
- State labels: `component.s0|propname|computedN`, `store.key`.
  Binding labels: `component#slot|#attr|#bind:tag`.

For the code table and the dev/prod build split, read
[Errors](/docs/errors/); for `t.inspect()` and `t.snapshot()` in tests,
read [Tests](/docs/tests/).