actualbudget/actual

[Maintenance] Add scoped ErrorBoundaries to isolate feature-level crashes

Open

#7,391 opened on Apr 5, 2026

View on GitHub
 (3 comments) (0 reactions) (0 assignees)JavaScript (603 forks)batch import
good first issuehelp wantedmaintenancetech debt

Repository metrics

Stars
 (7,129 stars)
PR merge metrics
 (Avg merge 4d 13h) (170 merged PRs in 30d)

Description

Summary

When a component throws a rendering error, the entire app crashes to the "Fatal Error" screen. This happens because we only have two top-level ErrorBoundary wrappers (in App.tsx) and one feature-scoped boundary (in GetCardData.tsx for report charts).

Adding scoped ErrorBoundary components around major features would contain failures to the affected area instead of taking down the whole app.

Problem

A single rendering error in any feature — budget, accounts, transactions, reports, schedules, rules — crashes the entire application. Users lose all context and must restart.

This is a recurring pattern. Across open and closed issues, there are 70+ reports of fatal crashes caused by component-level errors that could have been contained:

Currently open:

  • #7273 — Fatal Error after reports .json file import
  • #7285 — Fatal Error viewing ledgers with recurring transactions ending in the past
  • #7098 — Custom Report bars crash
  • #7108 — Calendar widget crashes on mobile
  • #6073 — Hide reconciled transactions crashes app
  • #7358 — Dollar sign in Schedule Name crashes app
  • #6317 — Backslash in rule notes causes crash
  • #5351 — API Unhandled Rejection

Recurring crash patterns from closed issues:

  • Filter condition changes (is ↔ one-of switching) — #6446, #6479, #6325, #6452, #6590, #5476, #5347
  • Reports/charts rendering — #6221, #5814, #5729, #5764, #6406, #1792, #4307
  • Rules and schedules — #6885, #6422, #6160, #5258, #4013, #3989, #3954, #2180
  • Date field handling — #5304, #5094, #5969
  • Mobile/browser-specific — #6650, #6467, #5692, #4204, #3263
  • Modals — #4703 (fixed by adding an ErrorBoundary — proving the pattern works)

The fix for #4703 (adding an ErrorBoundary around modals) shows this approach works. We should apply it systematically.

Proposed approach

Wrap major feature areas with ErrorBoundary from react-error-boundary (already a dependency at v6.0.3). Each boundary should:

  1. Catch and contain the error to that feature area
  2. Show a contextual fallback (e.g., "This report couldn't be loaded" instead of a full-app fatal screen)
  3. Offer a retry via resetErrorBoundary where it makes sense

A shared FeatureErrorFallback component now lives at packages/desktop-client/src/components/FeatureErrorFallback.tsx and is used by all completed boundaries — please reuse it for consistency.

Suggested areas to add boundaries

Desktop / shared

Status Area Entry point(s) Related crashes
Budget table BudgetTable / BudgetPageHeader (wrapped in DynamicBudgetTable.tsx) #5969, #6073
Account ledger Account component (accounts/Account.tsx) #7285, #3263, #595
Transaction list TransactionList (transactions/TransactionList.tsx) #5304, #1021
Reports (individual) Each full-page report view — NetWorth.tsx, CashFlow.tsx, Spending.tsx, CustomReport.tsx, Calendar.tsx, BalanceForecast.tsx, Sankey.tsx, Summary.tsx, AgeOfMoney.tsx, Crossover.tsx, Formula.tsx, BudgetAnalysis.tsx #7273, #7098, #5814, #5764, #6221
Schedules Schedules page (schedules/index.tsx) #7358, #3989, #3954
Rules Rules page (ManageRulesPage.tsx, plus route-level wrapping in FinancesApp.tsx) #6317, #6885, #4013, #2180
Sidebar Sidebar component (sidebar/Sidebar.tsx) #5467
Dashboard widgets Individual widget cards (per-card boundary in reports/Overview.tsx) #7108, #6313
Individual modals Each modal content (boundary in common/Modal.tsx) #4703 pattern

Mobile

None of the mobile entry points currently have feature-scoped boundaries — grep -r "ErrorBoundary" packages/desktop-client/src/components/mobile/ returns zero hits. Several of the linked crashes (#7108, #6650, #6467, #5692, #4204, #3263) are mobile-specific, so each of these pages should mirror its desktop counterpart.

Status Area Entry point(s)
🔴 Mobile budget mobile/budget/BudgetPage.tsx (wrapping BudgetTable)
🔴 Mobile category transactions mobile/budget/CategoryPage.tsx / CategoryTransactions.tsx
🔴 Mobile accounts list mobile/accounts/AccountsPage.tsx
🔴 Mobile account ledger mobile/accounts/AccountPage.tsx (and the *AccountTransactions.tsx variants)
🔴 Mobile transaction list mobile/transactions/TransactionList.tsx / TransactionListWithBalances.tsx
🔴 Mobile transaction edit mobile/transactions/TransactionEdit.tsx
🔴 Mobile schedules mobile/schedules/MobileSchedulesPage.tsx (and MobileScheduleEditPage.tsx)
🔴 Mobile rules mobile/rules/MobileRulesPage.tsx (and MobileRuleEditPage.tsx)
🔴 Mobile payees mobile/payees/MobilePayeesPage.tsx (and MobilePayeeEditPage.tsx)
🔴 Mobile bank sync mobile/banksync/MobileBankSyncPage.tsx (and MobileBankSyncAccountEditPage.tsx)

Implementation notes

  • react-error-boundary v6 is already a dependency — no new packages needed
  • FatalError.tsx has good patterns for error display that can be adapted
  • The shared FeatureErrorFallback component keeps UX consistent across boundaries
  • Each boundary can use onError to log to the existing notification system

This is a good first contribution

Each remaining boundary is a small, self-contained change — pick a report view or a mobile page from the tables above. The pattern is straightforward:

import { ErrorBoundary } from 'react-error-boundary';
import { FeatureErrorFallback } from '#components/FeatureErrorFallback';

<ErrorBoundary FallbackComponent={FeatureErrorFallback}>
  <YourFeatureComponent />
</ErrorBoundary>

Contributor guide