From 014d57c53cdec2a52c4d23a8e9e9b8be3113dd89 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Fri, 26 Jun 2026 20:51:42 +0200 Subject: [PATCH] feat(app): top-level error boundary so a render error no longer blanks the app (#1194) * feat(app): add a top-level error boundary so a render error no longer blanks the app Until now any thrown error during render took the whole window down with no recovery (issue #382). A class ErrorBoundary around the authenticated shell catches it and shows a recoverable fallback (Try again / Reload app) while playback keeps going. Hook-free + English-only with literal CSS var() fallbacks so it renders even when i18n/theme/state is what broke. * docs(changelog): error boundary prevents the app blanking on a render error (PR #1194) --- CHANGELOG.md | 6 +++ src/app/MainApp.tsx | 9 ++-- src/components/ErrorBoundary.tsx | 60 ++++++++++++++++++++++++ src/styles/components/error-boundary.css | 57 ++++++++++++++++++++++ src/styles/components/index.css | 1 + 5 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 src/components/ErrorBoundary.tsx create mode 100644 src/styles/components/error-boundary.css diff --git a/CHANGELOG.md b/CHANGELOG.md index 00854b22..74d2a7d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -337,6 +337,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * The AutoDJ overlap-cap and the Native Hi-Res blend-rate options in Settings → Audio now sit in the same bordered sub-card the Normalization options use, and the Hi-Res section no longer shows a double border. +### App no longer blanks on an unexpected error + +**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1194](https://github.com/Psychotoxical/psysonic/pull/1194)** + +* If a screen hit an unexpected rendering error, the whole window could go blank with no way back. The app now shows a small recoverable error card (Try again / Reload app) instead, and playback keeps going. + ## [1.48.1] - 2026-06-15 ## Fixed diff --git a/src/app/MainApp.tsx b/src/app/MainApp.tsx index fce705f9..da6d6895 100644 --- a/src/app/MainApp.tsx +++ b/src/app/MainApp.tsx @@ -32,6 +32,7 @@ import { useMigrationOrchestrator } from '../hooks/useMigrationOrchestrator'; import { IS_WINDOWS } from '../utils/platform'; import TauriEventBridge from './TauriEventBridge'; import AppShell from './AppShell'; +import ErrorBoundary from '../components/ErrorBoundary'; import BlockingMigrationGate from './BlockingMigrationGate'; import RequireAuth from './RequireAuth'; import { useMigrationStore } from '../store/migrationStore'; @@ -191,9 +192,11 @@ export default function MainApp() { path="/*" element={ - - - + + + + + } /> diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 00000000..cfcda29d --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -0,0 +1,60 @@ +import { Component, type ErrorInfo, type ReactNode } from 'react'; + +interface Props { + children: ReactNode; +} + +interface State { + error: Error | null; +} + +/** + * App-level error boundary. A render-time throw used to blank the entire window + * — no boundary existed (issue #382), so any thrown error during render took the + * whole UI down. This catches it and shows a recoverable fallback while playback + * (driven by the Rust audio engine, outside React) keeps going. + * + * Deliberately hook-free and English-only: the fallback has to render even when + * i18n, the theme, or app state is exactly what broke, so it must not depend on + * any of them. The CSS uses literal fallbacks in `var(...)` for the same reason. + */ +export default class ErrorBoundary extends Component { + state: State = { error: null }; + + static getDerivedStateFromError(error: Error): State { + return { error }; + } + + componentDidCatch(error: Error, info: ErrorInfo): void { + console.error('[error-boundary]', error, info.componentStack); + } + + private handleRetry = (): void => this.setState({ error: null }); + + private handleReload = (): void => window.location.reload(); + + render(): ReactNode { + const { error } = this.state; + if (!error) return this.props.children; + return ( +
+
+

Something went wrong

+

+ This view hit an unexpected error and stopped rendering. Playback keeps going — try the + view again, or reload the app. +

+
{error.message}
+
+ + +
+
+
+ ); + } +} diff --git a/src/styles/components/error-boundary.css b/src/styles/components/error-boundary.css new file mode 100644 index 00000000..a4b96030 --- /dev/null +++ b/src/styles/components/error-boundary.css @@ -0,0 +1,57 @@ +/* App-level error boundary fallback (components/ErrorBoundary.tsx). Literal + var() fallbacks so it renders even when the active theme is what broke. */ +.app-error-boundary { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + min-height: 60vh; + padding: var(--space-6, 24px); + background: var(--bg-app, #1e1e2e); + color: var(--text-primary, #cdd6f4); +} + +.app-error-boundary__card { + width: 100%; + max-width: 520px; + padding: var(--space-6, 24px); + background: var(--bg-card, #313244); + border: 1px solid var(--border, #45475a); + border-radius: var(--radius-lg, 12px); + box-shadow: var(--shadow-md, 0 8px 24px rgba(0, 0, 0, 0.3)); + text-align: center; +} + +.app-error-boundary__title { + margin: 0 0 var(--space-2, 8px); + font-size: 20px; + font-weight: 700; +} + +.app-error-boundary__text { + margin: 0 0 var(--space-4, 16px); + color: var(--text-secondary, #bac2de); + font-size: 14px; + line-height: 1.5; +} + +.app-error-boundary__detail { + margin: 0 0 var(--space-4, 16px); + padding: var(--space-3, 12px); + max-height: 160px; + overflow: auto; + background: var(--bg-deep, #181825); + border-radius: var(--radius-md, 8px); + color: var(--text-muted, #a6adc8); + font-size: 12px; + text-align: left; + white-space: pre-wrap; + word-break: break-word; +} + +.app-error-boundary__actions { + display: flex; + gap: var(--space-3, 12px); + justify-content: center; +} diff --git a/src/styles/components/index.css b/src/styles/components/index.css index add3c5d7..b5d1f40a 100644 --- a/src/styles/components/index.css +++ b/src/styles/components/index.css @@ -94,3 +94,4 @@ @import './np-dash.css'; @import './orbit-session-top-strip.css'; @import './back-to-top.css'; +@import './error-boundary.css';