Zero React error boundaries — any render exception in any component takes the whole app down with no recovery affordance #153

Closed
opened 2026-10-03 16:04:39 +00:00 by Mark · 1 comment
Owner

Finding

The app has zero React error boundaries. A runtime exception in any component (a bad image, a parse failure, a math overflow, a missing null check) results in a blank white screen with no recovery affordance. The user must refresh the page, losing any in-memory state.

Verified: grep -rn 'ErrorBoundary\|componentDidCatch\|getDerivedStateFromError' src/ returns no matches.

The closest thing to a defensive layer is the useWorkspace reducer's try/catch blocks around dispatch handlers, but those only protect the dispatch path, not the render path. A TypeError thrown during CanvasArea's render or EditorCanvas's event handler will crash the entire React tree.

Where

  • The whole src/ directory: no ErrorBoundary component, no componentDidCatch implementation, no getDerivedStateFromError static method
  • App layout: src/App.tsx — <Routes> is not wrapped in any boundary
  • Help page error messages: src/pages/Help.tsx:392 ("Search topics, stitch types, error messages...") — the help page advertises "error messages" as searchable content but the app has no structured error-message system to search

Suggested fix shape

Three layers:

  1. Root-level ErrorBoundary wrapping <Routes> in App.tsx. Catches any uncaught render error from any page. Shows a "Something went wrong — Reload" UI with the error message and a stack trace (in dev) or a generic message (in prod). Reload button calls window.location.reload().

  2. Section-level boundaries in each page that has complex state: Workspace, Editor, Gallery. A render error in the Gallery's DesignThumbnail shouldn't take down the user's edit in the Editor (if they were using modals across pages) or vice versa. The Editor and Workspace are each their own routes, so the root boundary is enough for that. But the Settings modal and the Export dialog are portaled — if they throw, the parent should catch.

  3. Error state inside useWorkspace for the digitize/export flow. The reducer's existing addToast calls cover user-facing errors. The error boundary covers the unexpected.

Severity

High. A single render error in any component takes the entire app down. The user has no recourse but a hard refresh. For a product that handles user-uploaded files (which can be malformed, oversized, corrupted), this is a real availability issue.

Discovered by

Gap analysis (2026-10-02) of app resilience.

  • The Help page mentions "error messages" as a thing users will search for. Without a structured error system and without a way to recover from errors, the promise is half-kept.
## Finding The app has **zero React error boundaries**. A runtime exception in any component (a bad image, a parse failure, a math overflow, a missing null check) results in a blank white screen with no recovery affordance. The user must refresh the page, losing any in-memory state. Verified: `grep -rn 'ErrorBoundary\|componentDidCatch\|getDerivedStateFromError' src/` returns no matches. The closest thing to a defensive layer is the `useWorkspace` reducer's `try/catch` blocks around dispatch handlers, but those only protect the dispatch path, not the render path. A `TypeError` thrown during `CanvasArea`'s render or `EditorCanvas`'s event handler will crash the entire React tree. ## Where - The whole `src/` directory: no `ErrorBoundary` component, no `componentDidCatch` implementation, no `getDerivedStateFromError` static method - App layout: `src/App.tsx` — `<Routes>` is not wrapped in any boundary - Help page error messages: `src/pages/Help.tsx:392` ("Search topics, stitch types, error messages...") — the help page advertises "error messages" as searchable content but the app has no structured error-message system to search ## Suggested fix shape Three layers: 1. **Root-level `ErrorBoundary`** wrapping `<Routes>` in `App.tsx`. Catches any uncaught render error from any page. Shows a "Something went wrong — Reload" UI with the error message and a stack trace (in dev) or a generic message (in prod). Reload button calls `window.location.reload()`. 2. **Section-level boundaries** in each page that has complex state: Workspace, Editor, Gallery. A render error in the Gallery's `DesignThumbnail` shouldn't take down the user's edit in the Editor (if they were using modals across pages) or vice versa. The Editor and Workspace are each their own routes, so the root boundary is enough for that. But the Settings modal and the Export dialog are portaled — if they throw, the parent should catch. 3. **Error state inside `useWorkspace`** for the digitize/export flow. The reducer's existing `addToast` calls cover user-facing errors. The error boundary covers the unexpected. ## Severity High. A single render error in any component takes the entire app down. The user has no recourse but a hard refresh. For a product that handles user-uploaded files (which can be malformed, oversized, corrupted), this is a real availability issue. ## Discovered by Gap analysis (2026-10-02) of app resilience. ## Related - The Help page mentions "error messages" as a thing users will search for. Without a structured error system and without a way to recover from errors, the promise is half-kept.
Author
Owner

Fixed in commit 85d342f on origin/main.

What landed:

  • New src/components/ErrorBoundary.tsx — class component with componentDidCatch + getDerivedStateFromError. Default fallback shows the scope name, the error message, and Try-again / Reload buttons; dev mode also shows the stack trace.
  • src/App.tsx now wraps <Routes> in a root ErrorBoundary scope="App" plus a per-route scoped boundary (Home / Workspace / Editor / Gallery / Help). A crash in one page leaves the navbar/footer usable so the user can navigate away.
  • The fallback prop accepts a custom React.ComponentType<{error, reset}> for callers that want to override the default panel.
  • componentDidCatch logs to console.error with a [ErrorBoundary:<scope>] tag — drop-in hook for a future Sentry / log layer.

Verification: 8 new tests cover the default fallback, custom fallback, recovery via reset, re-throw after reset, and nested-boundary isolation. Full suite: 642/642 pass. eslint clean, tsc clean, vite build clean.

Closes the gap: a render error in any component now produces a scoped fallback UI instead of a blank white screen with no recovery affordance.

Fixed in commit 85d342f on origin/main. **What landed:** - New `src/components/ErrorBoundary.tsx` — class component with `componentDidCatch` + `getDerivedStateFromError`. Default fallback shows the scope name, the error message, and Try-again / Reload buttons; dev mode also shows the stack trace. - `src/App.tsx` now wraps `<Routes>` in a root `ErrorBoundary scope="App"` plus a per-route scoped boundary (Home / Workspace / Editor / Gallery / Help). A crash in one page leaves the navbar/footer usable so the user can navigate away. - The `fallback` prop accepts a custom `React.ComponentType<{error, reset}>` for callers that want to override the default panel. - `componentDidCatch` logs to `console.error` with a `[ErrorBoundary:<scope>]` tag — drop-in hook for a future Sentry / log layer. **Verification:** 8 new tests cover the default fallback, custom fallback, recovery via reset, re-throw after reset, and nested-boundary isolation. Full suite: 642/642 pass. eslint clean, tsc clean, vite build clean. Closes the gap: a render error in any component now produces a scoped fallback UI instead of a blank white screen with no recovery affordance.
Mark closed this issue 2026-10-03 16:39:35 +00:00
Sign in to join this conversation.
No description provided.