Skip to content

fix(frontend): add error boundaries so one render throw doesn't blank the app - #159

Closed
lucastononro wants to merge 7 commits into
fix/101-typed-ssefrom
fix/102-error-boundary
Closed

fix(frontend): add error boundaries so one render throw doesn't blank the app#159
lucastononro wants to merge 7 commits into
fix/101-typed-ssefrom
fix/102-error-boundary

Conversation

@lucastononro

@lucastononro lucastononro commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • There was no app/error.tsx, app/global-error.tsx, or any ErrorBoundary in src/. Given untyped/unvalidated SSE payloads and live-rendered agent-authored markdown + self-contained HTML artifacts, one bad render throw took down the entire SPA with a blank screen.

Changes

  • src/app/error.tsx — route-level boundary (Next.js App Router convention: a file named error.tsx wraps its route segment, the whole app here since it's directly under src/app/). Shows a "Try again" (calls reset()) / "Reload page" recovery screen instead of a blank page.
  • src/app/global-error.tsx — root-layout boundary. error.tsx can't catch errors thrown by the root layout itself (or providers it mounts, e.g. AppProvider/ToastProvider), only global-error.tsx can, and to do so it must render its own <html>/<body> (it replaces the root layout while active). Intentionally dependency-free — inline-styled, no globals.css/Tailwind/app components, since those may be exactly what failed to mount.
  • src/components/ErrorBoundary.tsx — reusable class-component boundary (getDerivedStateFromError/componentDidCatch have no hook equivalent in React 18) with an optional custom fallback render-prop and a reset() callback.
  • Wrapped:
    • The workspace canvas (WorkspaceSidebar, keyed by sessionId so switching sessions also clears any prior crash state) — protects file viewing, the report tab, metrics, lineage, notebooks, and HTML artifacts as one unit.
    • Every markdown renderer individually, each with a graceful fallback that renders the raw text instead of a scary panel where that fits the surface: chat assistant bubbles (ChatItemView), sub-agent result summaries (SubAgentCard), the report tab (ReportMarkdown), and markdown file previews (FileViewer).

Test plan

Existing (must still work)

  • Streaming chat, live metrics, file/image/PDF viewing, and notebook updates all render exactly as before — this PR only adds boundaries around existing renderers, no logic changed. npm run build passes clean (next build compiles + prerenders all routes successfully).

New

  • Trigger a render throw inside the workspace canvas (e.g. temporarily throw from within a tab's render) — confirm only the canvas panel shows the fallback ("Couldn't render the workspace." + Try again), not a blank page, and the rest of the app (chat, sidebar) keeps working.
  • Trigger a render throw inside a markdown renderer (assistant bubble / sub-agent summary / report tab / file preview) — confirm that specific surface degrades to plain text (or the generic fallback for the report/file cases) rather than crashing the page.
  • Force a root-layout-level throw (e.g. temporarily throw in layout.tsx) — confirm global-error.tsx renders (its own minimal styled page, independent of Tailwind) with working "Try again"/"Reload page" buttons.

Closes #102

🤖 Generated with Claude Code

Greptile Summary

This PR adds three layers of error containment — error.tsx (route-level), global-error.tsx (root-layout-level), and a reusable ErrorBoundary class component — to prevent a single bad render from blanking the entire SPA. The boundaries are placed precisely where unvalidated SSE payloads and agent-authored content are rendered.

  • ErrorBoundary.tsx: named-export class component with getDerivedStateFromError/componentDidCatch, an optional fallback render-prop, and a generic recovery panel; wraps the workspace canvas (keyed by sessionId), file markdown (keyed by filePath), report tab, SubAgentCard summaries, and ChatItemView bubbles.
  • error.tsx / global-error.tsx: Next.js App Router conventions satisfied — route-level boundary lives within the root layout (Tailwind/lucide available), while the root-layout boundary renders its own <html>/<body> with inline styles only, independent of any provider that might have failed.
  • page.tsx: extracts truncatedSummary so both the normal markdown path and the error-boundary fallback render the same 800-character-capped text; FileViewer's boundary is keyed by filePath so switching files always resets stale error state.

Confidence Score: 5/5

Safe to merge — adds containment around existing renderers with no logic changes and no new data paths.

All three boundary layers follow Next.js App Router conventions correctly. Previously flagged issues are all addressed in this version of the PR.

Files Needing Attention: No files require special attention.

Important Files Changed

Filename Overview
frontend/src/components/ErrorBoundary.tsx New reusable class-component boundary with named export, getDerivedStateFromError/componentDidCatch, optional fallback render-prop, and a generic panel fallback with a reset button — implementation is correct.
frontend/src/app/error.tsx Route-level Next.js error boundary — correct default export, Tailwind-based UI, logs via useEffect, and both Try-again/Reload actions.
frontend/src/app/global-error.tsx Root-layout error boundary — renders its own html/body, intentionally dependency-free (inline styles only), logs via useEffect, and exposes Try-again/Reload.
frontend/src/app/page.tsx Wraps WorkspaceSidebar (keyed by sessionId), FileViewer markdown (keyed by filePath), ReportMarkdown, SubAgentCard, and ChatItemView markdown in boundaries; extracts truncatedSummary so both normal and error paths render the same capped text.
STAGING-v0.0.5.md Adds the row for PR #157 (fix/101-typed-sse) to the staging integration log — documentation only.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Render throw] --> B{Where did it occur?}
    B -->|Inside WorkspaceSidebar| C["ErrorBoundary (key=sessionId)\nWorkspace fallback panel\n+ Try again"]
    B -->|Inside FileViewer markdown| D["ErrorBoundary (key=filePath)\n'Couldn't render this file'\n+ Try again"]
    B -->|Inside ReportMarkdown| E["ErrorBoundary\n'Couldn't render this report'\n+ Try again"]
    B -->|Inside SubAgentCard| F["ErrorBoundary\nfallback: raw truncatedSummary text"]
    B -->|Inside ChatItemView| G["ErrorBoundary\nfallback: raw item.content text"]
    B -->|Escapes nested boundaries| H["error.tsx\nRoute-level boundary\nTailwind UI, logs via useEffect"]
    B -->|Root layout / providers throw| I["global-error.tsx\nRoot-layout boundary\nOwn html/body, inline styles only"]
    C --> J[Rest of app still works]
    D --> J
    E --> J
    F --> J
    G --> J
    H --> K[Full-page recovery screen]
    I --> L[Minimal inline-styled recovery screen]
Loading

Reviews (3): Last reviewed commit: "merge: staging-v0.0.5 into fix/102-error..." | Re-trigger Greptile

… the app

There was no app/error.tsx, app/global-error.tsx, or any ErrorBoundary in
src/ — with untyped SSE payloads and live-rendered agent-authored
markdown + self-contained HTML artifacts, one bad payload throwing in
render took down the entire SPA with a blank screen.

- src/app/error.tsx: route-level boundary (Next.js App Router convention)
  with a "Try again" / "Reload page" recovery screen.
- src/app/global-error.tsx: root-layout boundary with its own <html>/
  <body> and inline styles (no dependency on globals.css/Tailwind/app
  components, since those may be what failed to mount).
- src/components/ErrorBoundary.tsx: reusable class-component boundary
  (no hook equivalent exists for getDerivedStateFromError/componentDidCatch)
  with an optional custom fallback + reset callback.
- Wrapped the workspace canvas (WorkspaceSidebar, keyed by sessionId) and
  every markdown renderer (chat assistant bubbles, sub-agent result
  summaries, the report tab, and markdown file previews) so a crash is
  contained to that panel/bubble instead of the whole app.

Closes #102

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
Comment thread frontend/src/components/ErrorBoundary.tsx Outdated
Comment thread frontend/src/app/global-error.tsx
Comment thread frontend/src/app/page.tsx
- ErrorBoundary: default export -> named export, per the components/
  style guide (default export is reserved for page components).
- global-error.tsx: log the caught error via useEffect + console.error
  (matching error.tsx) so root-layout crashes aren't silent; both are
  zero-dependency builtins, keeping the boundary dependency-free.
- SubAgentCard: hoist the 800-char summary truncation so the error
  fallback renders the same bounded text as the ReactMarkdown path
  instead of dumping the full untruncated summary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
@lucastononro

Copy link
Copy Markdown
Owner Author

Greptile review follow-up (commit 11a197e)

Fixed (3/3 — all findings were valid)

  1. ErrorBoundary.tsx default export — switched to a named export (export class ErrorBoundary) and updated the import in page.tsx, per the components/AGENTS.md rule that default exports are reserved for page components.
  2. global-error.tsx never logged the error — added useEffect(() => { console.error('Global error boundary caught:', error); }, [error]), mirroring error.tsx. Both are zero-dependency builtins, so the boundary stays free of globals.css/Tailwind/app-component dependencies.
  3. SubAgentCard fallback skipped the 800-char truncation — hoisted the truncation into a truncatedSummary const used by both the ReactMarkdown path and the error fallback, so the two paths render the same bounded text and can't diverge again. (The assistant-bubble boundary was already consistent — both of its paths render full item.content — so no change there.)

Dismissed

None.

Tests

The frontend has no test framework (repo convention per AGENTS.md: frontend verification = npm run build && npm run lint + browser click-through; only the backend has pytest). Introducing a vitest stack in a review-fix commit would be a large, out-of-convention diff, so here is a manual test tutorial instead:

Manual test tutorial

Setup: docker compose up (or cd frontend && npm run dev against a running backend), open the app.

  1. Route-level boundary (error.tsx) — temporarily add throw new Error('test') at the top of the main page component's render, reload. Expect the "Something went wrong" screen with the error message, and Route-level error boundary caught: in the devtools console. "Try again" re-renders; "Reload page" reloads. Remove the throw.
  2. Root boundary (global-error.tsx) — run a production build (npm run build && npm start; global-error is not used by the dev overlay), temporarily throw inside layout.tsx/a provider. Expect the inline-styled "Trainable failed to load" screen (renders even though Tailwind/globals.css never mounted) and Global error boundary caught: in the console. Remove the throw.
  3. Markdown bubble containment — temporarily make the assistant-bubble ReactMarkdown throw (e.g. render {(() => { throw new Error('md'); })()} inside the boundary's children for one item). Expect only that bubble to fall back to plain text; the rest of the chat, sidebar, and workspace stay interactive.
  4. Sub-agent summary truncation — expand a completed sub-agent card whose result summary exceeds 800 chars. Expect the summary to end with ... at ~800 chars in the normal render; force the markdown path to throw (as in step 3) and confirm the plain-text fallback is also truncated to the same ~800 chars.
  5. Workspace canvas boundary — throw inside WorkspaceSidebar's render. Expect the "Couldn't render the workspace" panel with a working "Try again" button while chat keeps working; switching sessions (the boundary is keyed by sessionId) also clears it.

Validation

npx tsc --noEmit, npm run lint, npm run build — all pass.

Comment thread frontend/src/app/page.tsx Outdated
…ptile P1 on #159)

FileViewer is memoized and keeps its component instance across file
switches, so a markdown render crash left the boundary stuck in its error
fallback for every subsequent file. key={filePath} remounts the boundary
per file.
Conflict in page.tsx FileViewer markdown block: kept #159's ErrorBoundary
wrapper (with key={filePath} P1 fix) AND staging's api.filesRawUrl helper
(#133). Also prettier --write on global-error.tsx/ErrorBoundary.tsx
(branch predates the CI format gate).
lucastononro added a commit that referenced this pull request Jul 29, 2026
lucastononro added a commit that referenced this pull request Jul 29, 2026
@lucastononro

Copy link
Copy Markdown
Owner Author

Merged into integration branch staging-v0.0.5 (see the STAGING-v0.0.5.md ledger on that branch for merge order, Greptile follow-ups and test results). These changes will land on main via the staging merge — closing to clear the queue.

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