Skip to content

fix(app): show a recovery screen instead of a blank window when a render throws - #3402

Open
pranshu26 wants to merge 1 commit into
different-ai:devfrom
pranshu26:fix/app-root-error-boundary
Open

fix(app): show a recovery screen instead of a blank window when a render throws#3402
pranshu26 wants to merge 1 commit into
different-ai:devfrom
pranshu26:fix/app-root-error-boundary

Conversation

@pranshu26

Copy link
Copy Markdown

Summary

  • Add a last-resort React error boundary around the whole app, so a render throw shows a recovery screen with the error and stack instead of a blank window.

Why

React unmounts the entire tree when a render throws. The renderer has only two boundaries today — LexicalErrorBoundary (composer/editor.tsx:1249) and the tool-part boundary in components/chat/message-list.tsx:148 — and nothing above the routes. The comment on that second one already describes this failure mode:

Tool inputs from streamed or interrupted runs can violate their type contracts (partial/undefined input); without this boundary a single bad part unmounts the entire app (white screen). Seen in production on v0.15.3 via a todowrite part with missing input.todos.

The same shape reaches the whole app from route-level render work. #3372 is a live example: a local MCP server configured with a string command throws config.command?.some is not a function inside a useMemo in settings-route.tsx:1797, and the user gets a blank window with nothing to report. composer.tsx:1605 throws the same way and takes out the main chat screen.

This does not fix any individual crash — #3373 is the right fix for #3372 specifically. It changes what a crash looks like: an error card with a copyable stack instead of a white screen, which also makes the next one of these reportable.

Issue

Scope

  • apps/app/src/react-app/shell/app-error-boundary.tsx (new) — the boundary and its fallback UI.
  • apps/app/src/index.react.tsx — wrap the tree, inside StrictMode so provider failures are caught too.
  • apps/app/tests/app-error-boundary.test.tsx (new).

Out of scope

Testing

Ran

  • bun test --isolate tests/app-error-boundary.test.tsx
  • bun test --isolate tests/ (full app suite)
  • pnpm --filter @openwork/app typecheck

Result

  • pass: 4/4 new tests.
  • pass: full app suite 580 pass / 0 fail across 105 files.
  • pass: typecheck exits 0.

One note on how the tests are written: react-dom/server rethrows instead of running error boundaries, so renderToStaticMarkup(<Boundary><Throws/></Boundary>) cannot exercise the catch path — I tried that first and it fails. The tests instead drive the state transition directly (getDerivedStateFromErrorrender()), which covers the same code the boundary runs. The actual catching is verified in the running app below.

Manual verification

  1. pnpm --filter @openwork/app dev
  2. Temporarily rendered a component that throws config.command?.some is not a function inside the router, simulating Settings page renders blank/white when a local MCP server uses string command (config.command?.some is not a function) #3372.
  3. Loaded the app.

Before: blank window, nothing rendered, no indication of what happened.
After: the recovery screen renders — heading "OpenWork hit an unexpected error", the full stack in a scrollable block (at Boom (http://localhost:5199/src/index.react.tsx:28:9) etc.), and working Reload / Copy details buttons. Correct in dark theme.

The throwing component was removed before committing; the diff contains no test scaffolding.

CI status

  • pass: n/a — the test workflow does not run on this PR.
  • code-related failures: none observed locally.
  • external/env/auth blockers: yes. As a fork PR, OpenWork Tests and i18n Audit sit in action_required pending maintainer approval, and the Vercel checks fail with "Authorization required to deploy". Both are independent of this diff.

Evidence

  • Screenshot of the recovery screen available on request — I can attach it to this thread if useful.

Risk

Low. The boundary is inert until something throws; the passthrough case is covered by a test asserting the children render byte-identically. Worst case is a cosmetic issue on a screen only reachable when the app has already broken.

Two deliberate choices worth flagging for review:

  • It renders plain elements rather than @/components primitives, and does not call t(). initLocale() runs before the tree mounts and has itself been a startup failure (Windows Electron 启动时序问题:LevelDB 在 initLocale() 执行时尚未完全加载,导致回退到英文 #2767), so the screen that reports a crash shouldn't depend on i18n or on component code that may be implicated in the crash. This means the strings are English-only — tell me if you'd rather take the i18n dependency and I'll switch it.
  • It's placed inside StrictMode but outside QueryClientProvider, so provider-level failures are caught too.

Rollback

Revert the commit — remove the wrapper in index.react.tsx and delete the two new files. Nothing else imports the boundary.

…der throws

React unmounts the whole tree when a render throws, and the renderer had no
boundary above the routes — only LexicalErrorBoundary in the composer and the
tool-part boundary in components/chat/message-list.tsx. That second one already
documents this failure mode in its own comment ("without this boundary a single
bad part unmounts the entire app (white screen). Seen in production on v0.15.3").

The same shape reaches the whole app from route-level render work. different-ai#3372 is a
current example: a local MCP server configured with a string `command` throws
`config.command?.some is not a function` inside a `useMemo` in settings-route,
and the user gets a blank window with nothing to report.

This adds a last-resort boundary around the tree that shows the error, its
stack, a Reload button, and Copy details. It renders plain elements and does not
call `t()` on purpose: locale init runs before the tree mounts and has itself
been a startup failure (different-ai#2767), so the screen that reports a crash must not
depend on it.

This does not fix any individual crash — it stops them presenting as a blank
window.

Co-Authored-By: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openwork-landing Ready Ready Preview, v0 Jul 31, 2026 11:15pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
openwork-diagnostics Skipped Skipped Jul 31, 2026 11:15pm

@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Someone is attempting to deploy a commit to the Different AI Team on Vercel.

A member of the Team first needs to authorize it.

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