Skip to content

fix: surface reconcile subscription failures via console.error#45

Merged
jonlaing merged 2 commits into
mainfrom
fix/outlet-surface-errors
Jul 24, 2026
Merged

fix: surface reconcile subscription failures via console.error#45
jonlaing merged 2 commits into
mainfrom
fix/outlet-surface-errors

Conversation

@jonlaing

Copy link
Copy Markdown
Owner

Summary

Answer to "does Outlet swallow errors?" — yes, and this PR stops it.

Where the swallow was

All three `ControlCtx` implementations had:

```ts
subscribe: (readable, handler) =>
Effect.gen(function* () {
const scope = yield* Effect.scope;
yield* readable.changes.pipe(
Stream.runForEach(handler),
Effect.forkIn(scope),
);
}),
```

Right lifecycle model — the stream reader lives with the parent scope and cleans up on unmount. Wrong error handling: `Effect.forkIn` swallows fiber failures. If `handler` ever failed (a route render threw, a data provider died mid-navigation, a slot insertion errored), the fiber died and nothing surfaced. UI froze; console stayed clean.

Reconcile's subscription is what drives `Outlet`'s re-render on `nav.pathname` changes, so this specifically explains the "silent navigation failure" pattern.

Fix

Extracted a shared `subscribeReconcile` helper (`packages/dom/src/Control/subscribeReconcile.ts`). All three `ControlCtx` `subscribe` fields now point at it. It:

  1. Logs each failing handler run via `console.error` with a full `Cause.pretty` (fiber trace + underlying errors). Devtools + error trackers pick it up automatically.
  2. Catches at the per-value boundary so a single bad update doesn't kill the subscription. Navigate to a broken route → error is logged → navigate back → recovers.

Test plan

  • 3 unit tests in `subscribeReconcile.test.ts`: failing handler produces `console.error`; subsequent updates still fire after a failure; silent when handler succeeds.
  • Full suite: 747 pass / 2 skipped (was 744 / 2)
  • `npx tsc --noEmit` clean

Related

Third in a small series of "make silent failures loud" changes:

Together these cover the three "your UI is broken but the console is empty" traps in the framework.

🤖 Generated with Claude Code

ClientControlCtx and both HydrationControlCtx factories implemented subscribe(readable, handler) by forking the stream reader into the parent scope via Effect.forkIn. That's the right lifecycle model but forkIn swallows fiber failures — if handler ever failed (a route render threw, a data provider died mid-navigation, a slot insertion errored) the fiber died silently and the UI froze with no console output.

Extracted a shared subscribeReconcile helper that logs each failing handler run with a full Effect Cause.pretty (fiber trace + underlying errors) and catches the failure at the per-value boundary so a single bad update doesn't kill the subscription — subsequent state changes still fire the handler. Navigation to a broken route and back now recovers.

Reconcile subscription is what drives Outlet's re-render on nav.pathname changes, so this specifically fixes silent navigation failures reported downstream.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 24, 2026

Copy link
Copy Markdown

Deploying effex with  Cloudflare Pages  Cloudflare Pages

Latest commit: caff662
Status: ✅  Deploy successful!
Preview URL: https://838e2d3a.effex.pages.dev
Branch Preview URL: https://fix-outlet-surface-errors.effex.pages.dev

View logs

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 24, 2026

Copy link
Copy Markdown

Deploying effex-api with  Cloudflare Pages  Cloudflare Pages

Latest commit: caff662
Status: ✅  Deploy successful!
Preview URL: https://af610740.effex-api.pages.dev
Branch Preview URL: https://fix-outlet-surface-errors.effex-api.pages.dev

View logs

Effect's Console service is idiomatic in an Effect-native codebase — it runs inside the fiber, returns Effect<void> (no need to wrap in Effect.sync), and can be swapped via Layer for tests. Default sink delegates to the real console, so existing spy-on-console tests keep working.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jonlaing
jonlaing merged commit 4724898 into main Jul 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant