Isolation Makes the Error Channel an Oracle
Twenty-five browser tools, three instruments that disagreed, and one logger that broke the tests it was added to help
Table of Contents
- 1. The problem, stated narrowly
- 2. Why the console was unreadable
- 3. What was built
- 4. Three instruments, three answers
- 5. The instrument perturbed the measurement
- 6. How much of this is new
- 6.1. The architecture is Playwright's current recommendation
- 6.2. The assertion ships in the tool that was already being used
- 6.3. "Console is clean" is thoroughly trodden
- 6.4. The random walk has an academic ancestor
- 6.5. Even the annotation has a name, and it is twenty-five years old
- 6.6. What survives
- 7. Open items
1. The problem, stated narrowly
This site publishes twenty-five browser tools — ClojureScript single-page apps compiled by shadow-cljs and served inside org-published pages. Two of them broke in production this month. Both presented identically: a blank panel where the tool should be.
Both were JS exceptions thrown during mount. primality-test threw
a.replace is not a function; counting-change called Math.LN2 as though
it were a function. In each case the browser said exactly what was wrong, in
the console, immediately.
Nobody was reading the console, and there is a structural reason why.
2. Why the console was unreadable
A published page here loads the tool's bundle and the site's advertising and analytics bundle. That second one logs its own errors, routinely, as a matter of course.
So on a published page the question "were there JavaScript errors?" has no useful answer. There are always errors. A gate asserting zero console errors would fail on every page forever, which means it would be turned off within a day, which means it would never have caught either defect.
This is the whole argument of the note, and it is worth stating as a general claim rather than a local one:
An error channel shared with code you do not control is not a signal. It becomes one only when everything else in the page is removed.
3. What was built
A harness that serves each tool with its own CSS and nothing else — no site chrome, no org-publish, no analytics. Twenty-five pages on one static port.
The mechanical parts were unremarkable and are not the finding. Ten tools had no harness page at all, so they could not be opened in isolation; ten more rendered unstyled because their page omitted the stylesheet. Both were generated from the build config so the ports cannot drift from it.
What the isolation bought is the finding: in that harness, a console error is attributable. Nothing else is running. The channel became an oracle, in the sense the parent note uses — a thing that can tell you that you are wrong without being told what right looks like (The Oracle Is the Deliverable, which argues hermeticity is a condition rather than a practice; this is what the condition buys in a browser).
4. Three instruments, three answers
The same two pages, the same afternoon:
| instrument | drove | found |
|---|---|---|
| Bombadil (random walk, LTL) | 117 states, 41 clicks | 0 violations |
| Playwright (scripted) | 8 clicks | 3 failures across 2 tools |
| the local beacon (JS hooks) | captured all 49 clicks | 0 errors |
All three are correct. They disagree because they listen to different things, and the differences are worth naming because each one is a trap.
4.1. The beacon cannot hear the browser
The beacon installs window.onerror, an unhandledrejection listener, and a
console.error wrapper. That covers everything page JavaScript does.
It does not cover what the browser says. The defect found that afternoon was
an SVG <line> emitted with an empty x1 — Unexpected end of attribute.
Expected length — which the rendering engine reports directly to the
console without passing through console.error. A JS-level hook is
structurally incapable of seeing it.
The same applies to CSP violations and failed subresource loads. Only a
CDP-level listener — Playwright's page.on('console'), or Bombadil —
catches those.
The practical consequence: if you annotate a problem and the beacon log shows no errors, that does not mean the page was clean. It means page JS did not throw.
4.2. The random walk drove five times harder and found nothing
Bombadil made 41 clicks to Playwright's 8 and reported no violations on a page Playwright failed three times. Two candidate explanations, and I have not separated them: either its console capture has the same blind spot, or twenty seconds of random clicking never reached the state Playwright reaches deliberately in two steps — the timing diagram, which is where the bad coordinate is drawn.
That is an open item and it matters. A property that cannot fire is not a property, and this one has not yet been shown to fire on a page known to be broken.
5. The instrument perturbed the measurement
The beacon was added to make debugging faster. It made the test suite lie.
Under four parallel Playwright workers, the beacon's sendBeacon POSTs
outran the small development server. The browser logged Failed to load resource:
net::ERR_CONNECTION_RESET, and the suite's own zero-console-errors
assertion reported that as a defect in primality-test — a tool that was,
by then, fine.
So the logger added to find defects manufactured one, in the first tool it touched, and the failure was indistinguishable in shape from a real finding.
The fix is to fulfil the beacon endpoint in-process during tests: the page's beacon code runs exactly as it does for a person, and nothing crosses the network. The rule it suggests is stronger than the fix:
Anything instrumenting a test harness must be inert under automation, not merely tolerated by it.
With that noise removed the suite reported two broken tools rather than one,
and both fail the same way — picture-language and digital-circuits each
emit <line> with empty coordinates. One bug class in two places is a better
lead than either failure alone, and it was invisible while the false positive
was in the way.
6. How much of this is new
Very little, and the parts that are not are worth naming precisely.
6.1. The architecture is Playwright's current recommendation
Playwright removed its experimental component-testing packages. The replacement is a story-gallery page served by your own development server, with Playwright navigating to it — on the stated grounds that Playwright does not compile or serve anything, it navigates to a page. That is the harness described above, arrived at independently and not first.
6.2. The assertion ships in the tool that was already being used
Bombadil's default properties include noConsoleErrors, noUncaughtExceptions,
noUnhandledPromiseRejections and noHttpErrorCodes. The error-channel
assertion was not added to Bombadil here; it is Bombadil, unmodified, finally
pointed at a page where it could mean something.
6.3. "Console is clean" is thoroughly trodden
Storybook's test-runner has --failOnConsole. Cypress fails on uncaught
exceptions by default. jest-fail-on-console and vitest-fail-on-console
exist and are widely used.
6.4. The random walk has an academic ancestor
Quickstrom (PLDI 2022) checked QuickLTL properties over DOM states, which is Bombadil's shape. It is dormant. Further back: gremlins.js explicitly goal-stated "triggering JavaScript errors", and the GUI event-flow-graph literature runs to Memon in the early 2000s.
6.5. Even the annotation has a name, and it is twenty-five years old
mark("what I was doing") is session-based test management (Bach, 2000):
notes taken during a session rather than reconstructed after it.
6.6. What survives
Three claims, all narrow:
- The ClojureScript ecosystem has no equivalent. Portfolio isolates genuinely — each scene renders in an iframe — and is maintained, but carries only a React error boundary and no global error hook. devcards, cljs-test-display, kaocha-cljs2 and dirac are dormant. Etaoin can read Chrome's log. No ClojureScript tool asserts on the error channel.
- The target is unusual. Component workshops exist for design systems. These are twenty-five standalone SPAs on a static content site whose published pages carry analytics — which is the condition that makes the channel unusable in the first place.
- The inference is not written down anywhere I could find. Everyone builds the harness. Everyone justifies it with style leakage and build speed. Sentry documents the noisy-console diagnosis exactly, as a production monitoring problem with a filtering remedy. Nobody appears to argue isolation is what makes the error channel an oracle and design the harness for that reason.
The third is a claim about absence, from one search pass, and should be held loosely.
7. Open items
- Whether Bombadil's
noConsoleErrorscan fire on the known-broken page, or whether the random walk simply never reaches the drawing state. Until that is settled the property is uncalibrated. - The shared
<line>defect in two tools. - Whether shadow-cljs
:browser-testsurfaces uncaught exceptions as failures. The Users Guide is silent and it was not tested.