diff --git a/apps/web/src/components/ErrorBoundary.tsx b/apps/web/src/components/ErrorBoundary.tsx
new file mode 100644
index 0000000..12a813a
--- /dev/null
+++ b/apps/web/src/components/ErrorBoundary.tsx
@@ -0,0 +1,54 @@
+import { Component, type ErrorInfo, type ReactNode } from 'react';
+
+interface Props {
+ children: ReactNode;
+ /** Optional fallback rendered in place of `children` when an error is caught. */
+ fallback?: (error: Error) => ReactNode;
+}
+
+interface State {
+ error: Error | null;
+}
+
+/**
+ * Catches render errors in its subtree and renders a fallback instead of
+ * letting them crash the whole page. Used to isolate failures in object
+ * viewers (e.g. WebGL context loss, model/texture load errors).
+ */
+export default class ErrorBoundary extends Component {
+ override state: State = { error: null };
+
+ static getDerivedStateFromError(error: Error): State {
+ return { error };
+ }
+
+ override componentDidCatch(error: Error, info: ErrorInfo) {
+ // Surface the error for debugging without breaking the UI.
+ console.error('Viewer error:', error, info.componentStack);
+ }
+
+ override render() {
+ if (this.state.error) {
+ return this.props.fallback
+ ? this.props.fallback(this.state.error)
+ : ;
+ }
+ return this.props.children;
+ }
+}
+
+function DefaultFallback({ error }: { error: Error }) {
+ return (
+
+
Couldn't render this object
+
+ {error.message}
+
+ {error.stack && (
+
+ {error.stack}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/pages/ModPage.tsx b/apps/web/src/pages/ModPage.tsx
index 9433506..3f0caf6 100644
--- a/apps/web/src/pages/ModPage.tsx
+++ b/apps/web/src/pages/ModPage.tsx
@@ -6,6 +6,7 @@ import { useModStore } from '../stores/modStore';
import { useSearchStore } from '../stores/searchStore';
import { modFileUrl } from '../api';
import ObjectTree from '../components/ObjectTree';
+import ErrorBoundary from '../components/ErrorBoundary';
import { resolveViewer } from '../components/viewers';
import { iconsForObject } from '../components/objectIcons';
// Register the 3D viewers (side effect) so `resolveViewer` can find them.
@@ -78,15 +79,17 @@ export default function ModPage() {
{Viewer && (
-
- Loading viewer…
-
- }
- >
-
-
+
+
+ Loading viewer…
+
+ }
+ >
+
+
+
)}
>
) : (