Debugging
The debugging loop for “my update did not happen” is three lines: arm, act, read:
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.
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.logarchaeology 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 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 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:
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); 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
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-xxxfailures, 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; for t.inspect() and t.snapshot() in tests,
read Tests.