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.
+