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

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 x1Unexpected 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:

  1. 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.
  2. 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.
  3. 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 noConsoleErrors can 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-test surfaces uncaught exceptions as failures. The Users Guide is silent and it was not tested.