[Maintenance] Add scoped ErrorBoundaries to isolate feature-level crashes
#7391 opened on Apr 5, 2026
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:
- Catch and contain the error to that feature area
- Show a contextual fallback (e.g., "This report couldn't be loaded" instead of a full-app fatal screen)
- Offer a retry via
resetErrorBoundarywhere 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-boundaryv6 is already a dependency — no new packages neededFatalError.tsxhas good patterns for error display that can be adapted- The shared
FeatureErrorFallbackcomponent keeps UX consistent across boundaries - Each boundary can use
onErrorto 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>