diff --git a/EngineDesign/frontend/src/App.tsx b/EngineDesign/frontend/src/App.tsx
index a509dd39..2feac84d 100644
--- a/EngineDesign/frontend/src/App.tsx
+++ b/EngineDesign/frontend/src/App.tsx
@@ -13,6 +13,7 @@ import ConfigurationSelector from './components/ConfigurationSelector';
import { emitConfigChanged } from './lib/configBus';
import { useViewState } from './lib/viewState';
import { DesignVersions } from './components/DesignVersions';
+import { ErrorBoundary } from './components/ErrorBoundary';
import { ReadOnlyProvider } from '@stardesign-ui';
import { getConfig, getHealth } from './api/client';
import type { EngineConfig } from './api/client';
@@ -237,117 +238,135 @@ function App() {
{/* Keep all tab panels mounted; hide inactive ones to preserve state */}
-
- {!config && (
-
-
Load Configuration
-
-
- )}
-
-
+
+
+ {!config && (
+
+
Load Configuration
+
+
+ )}
+
+
+
-
- {!config && (
-
-
Load Configuration
-
-
- )}
-
-
+
+
+ {!config && (
+
+
Load Configuration
+
+
+ )}
+
+
+
-
+
+
+
-
- {!config && (
-
-
Load Configuration
-
-
- )}
-
-
+
+
+ {!config && (
+
+
Load Configuration
+
+
+ )}
+
+
+
-
- {!config && (
-
-
Load Configuration
-
-
- )}
-
-
+
+
+ {!config && (
+
+
Load Configuration
+
+
+ )}
+
+
+
-
- {!config && (
-
-
Load Configuration
-
-
- )}
-
-
+
+
+ {!config && (
+
+
Load Configuration
+
+
+ )}
+
+
+
-
- {!config && (
-
-
Load Configuration
-
-
- )}
-
-
+
+
+ {!config && (
+
+
Load Configuration
+
+
+ )}
+
+
+
-
- {!config && (
-
-
Load Configuration
-
-
- )}
-
-
+
+
+ {!config && (
+
+
Load Configuration
+
+
+ )}
+
+
+
-
- {/* Upload section - compact */}
-
-
-
-
-
- {config && (
-
-
- Config loaded and ready
+
+
+ {/* Upload section - compact */}
+
+
+
+
- )}
+ {config && (
+
+
+ Config loaded and ready
+
+ )}
+
-
- {/* Editor section - full width */}
-
-
+ {/* Editor section - full width */}
+
+
+
-
+
diff --git a/EngineDesign/frontend/src/components/ErrorBoundary.tsx b/EngineDesign/frontend/src/components/ErrorBoundary.tsx
new file mode 100644
index 00000000..d9e2e1d3
--- /dev/null
+++ b/EngineDesign/frontend/src/components/ErrorBoundary.tsx
@@ -0,0 +1,75 @@
+import { Component, type ErrorInfo, type ReactNode } from 'react';
+
+/**
+ * Catches render-time exceptions so one bad value cannot blank the whole app.
+ *
+ * Without a boundary anywhere in the tree, React unmounts EVERYTHING when a
+ * render throws -- the user sees a white page with no message, no stack, and no
+ * way to report what happened. Every render bug then looks identical, which is
+ * exactly how "press Optimize, page goes blank" got reported with nothing to go
+ * on. This keeps the failure on screen and legible instead.
+ */
+interface Props {
+ children: ReactNode;
+ /** Shown above the error, e.g. "Flight Simulation". */
+ label?: string;
+}
+interface State {
+ error: Error | null;
+ info: ErrorInfo | null;
+}
+
+export class ErrorBoundary extends Component {
+ state: State = { error: null, info: null };
+
+ static getDerivedStateFromError(error: Error): Partial {
+ return { error };
+ }
+
+ componentDidCatch(error: Error, info: ErrorInfo) {
+ // Keep the console record: the boundary stops the crash from propagating,
+ // so without this the stack would be swallowed entirely.
+ console.error('[ErrorBoundary]', this.props.label ?? '', error, info.componentStack);
+ this.setState({ info });
+ }
+
+ private reset = () => this.setState({ error: null, info: null });
+
+ render() {
+ const { error, info } = this.state;
+ if (!error) return this.props.children;
+
+ const detail = [error.stack || String(error), info?.componentStack]
+ .filter(Boolean)
+ .join('\n\nComponent stack:');
+
+ return (
+
+
+ {this.props.label ? `${this.props.label} hit an error` : 'Something went wrong'}
+
+
+ The rest of the app is still running. Copy the detail below when reporting this.
+
+
{String(error.message || error)}
+
+
+ Show stack
+
+
+ {detail}
+
+
+
+
+ );
+ }
+}
+
+export default ErrorBoundary;
diff --git a/EngineDesign/frontend/src/lib/gating.test.ts b/EngineDesign/frontend/src/lib/gating.test.ts
index 4aa7f619..74474c97 100644
--- a/EngineDesign/frontend/src/lib/gating.test.ts
+++ b/EngineDesign/frontend/src/lib/gating.test.ts
@@ -79,6 +79,7 @@ const VIEW_ONLY: Record = {
'ConfigEditor.tsx:setSearchQuery': 'filters which sections are shown',
'ConfigEditor.tsx:setIsExpanded': 'expand/collapse a section',
'ConfigUpload.tsx:label': 'the drop zone wrapper, not a control',
+ 'ErrorBoundary.tsx:this.reset': 'clears a caught render error; touches no design state',
'Layer1Optimization.tsx:setShowParameterPlots': 'chart visibility',
'Layer1Optimization.tsx:setShowInjectorPressures': 'chart visibility',
'Layer1Optimization.tsx:setShowSolverInputsEcho': 'diagnostics visibility',