actualbudget/actual

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

Open

#7391 opened on Apr 5, 2026

View on GitHub
 (2 comments) (1 reaction) (0 assignees)JavaScript (7,129 stars) (603 forks)batch import
good first issuehelp wantedmaintenancetech debt

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