diff --git a/CHANGELOG.md b/CHANGELOG.md
index 25a83af..498c533 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- **Better page titles.** Browser tabs now show which site and page you're on (e.g. "Uptime · example.com | Pulse") instead of the same generic title everywhere.
- **Link previews for public dashboards.** Sharing a public dashboard link on social media now shows a proper preview with the site name and description.
- **Faster login redirects.** If you're not signed in and try to open a dashboard or settings page, you're redirected to login immediately instead of seeing a blank page first. Already-signed-in users who visit the login page are sent straight to the dashboard.
+- **Graceful error recovery.** If a page crashes, you now see a friendly error screen with a "Try again" button instead of a blank white page. Each section of the app has its own error message so you know exactly what went wrong.
## [0.10.0-alpha] - 2026-02-21
diff --git a/app/error.tsx b/app/error.tsx
new file mode 100644
index 0000000..4b1380b
--- /dev/null
+++ b/app/error.tsx
@@ -0,0 +1,13 @@
+'use client'
+
+import ErrorDisplay from '@/components/ErrorDisplay'
+
+export default function GlobalError({ reset }: { error: Error; reset: () => void }) {
+ return (
+
+ )
+}
diff --git a/app/notifications/error.tsx b/app/notifications/error.tsx
new file mode 100644
index 0000000..8a14fd3
--- /dev/null
+++ b/app/notifications/error.tsx
@@ -0,0 +1,13 @@
+'use client'
+
+import ErrorDisplay from '@/components/ErrorDisplay'
+
+export default function NotificationsError({ reset }: { error: Error; reset: () => void }) {
+ return (
+
+ )
+}
diff --git a/app/org-settings/error.tsx b/app/org-settings/error.tsx
new file mode 100644
index 0000000..f9889a3
--- /dev/null
+++ b/app/org-settings/error.tsx
@@ -0,0 +1,13 @@
+'use client'
+
+import ErrorDisplay from '@/components/ErrorDisplay'
+
+export default function OrgSettingsError({ reset }: { error: Error; reset: () => void }) {
+ return (
+
+ )
+}
diff --git a/app/share/[id]/error.tsx b/app/share/[id]/error.tsx
new file mode 100644
index 0000000..9756b5f
--- /dev/null
+++ b/app/share/[id]/error.tsx
@@ -0,0 +1,13 @@
+'use client'
+
+import ErrorDisplay from '@/components/ErrorDisplay'
+
+export default function ShareError({ reset }: { error: Error; reset: () => void }) {
+ return (
+
+ )
+}
diff --git a/app/sites/[id]/error.tsx b/app/sites/[id]/error.tsx
new file mode 100644
index 0000000..e1ea50a
--- /dev/null
+++ b/app/sites/[id]/error.tsx
@@ -0,0 +1,13 @@
+'use client'
+
+import ErrorDisplay from '@/components/ErrorDisplay'
+
+export default function DashboardError({ reset }: { error: Error; reset: () => void }) {
+ return (
+
+ )
+}
diff --git a/app/sites/[id]/funnels/error.tsx b/app/sites/[id]/funnels/error.tsx
new file mode 100644
index 0000000..2be69ce
--- /dev/null
+++ b/app/sites/[id]/funnels/error.tsx
@@ -0,0 +1,13 @@
+'use client'
+
+import ErrorDisplay from '@/components/ErrorDisplay'
+
+export default function FunnelsError({ reset }: { error: Error; reset: () => void }) {
+ return (
+
+ )
+}
diff --git a/app/sites/[id]/realtime/error.tsx b/app/sites/[id]/realtime/error.tsx
new file mode 100644
index 0000000..77bb93a
--- /dev/null
+++ b/app/sites/[id]/realtime/error.tsx
@@ -0,0 +1,13 @@
+'use client'
+
+import ErrorDisplay from '@/components/ErrorDisplay'
+
+export default function RealtimeError({ reset }: { error: Error; reset: () => void }) {
+ return (
+
+ )
+}
diff --git a/app/sites/[id]/settings/error.tsx b/app/sites/[id]/settings/error.tsx
new file mode 100644
index 0000000..5f53d0b
--- /dev/null
+++ b/app/sites/[id]/settings/error.tsx
@@ -0,0 +1,13 @@
+'use client'
+
+import ErrorDisplay from '@/components/ErrorDisplay'
+
+export default function SiteSettingsError({ reset }: { error: Error; reset: () => void }) {
+ return (
+
+ )
+}
diff --git a/app/sites/[id]/uptime/error.tsx b/app/sites/[id]/uptime/error.tsx
new file mode 100644
index 0000000..87bd036
--- /dev/null
+++ b/app/sites/[id]/uptime/error.tsx
@@ -0,0 +1,13 @@
+'use client'
+
+import ErrorDisplay from '@/components/ErrorDisplay'
+
+export default function UptimeError({ reset }: { error: Error; reset: () => void }) {
+ return (
+
+ )
+}
diff --git a/components/ErrorDisplay.tsx b/components/ErrorDisplay.tsx
new file mode 100644
index 0000000..6bed625
--- /dev/null
+++ b/components/ErrorDisplay.tsx
@@ -0,0 +1,63 @@
+'use client'
+
+import { Button } from '@ciphera-net/ui'
+
+interface ErrorDisplayProps {
+ title?: string
+ message?: string
+ onRetry?: () => void
+ onGoHome?: boolean
+}
+
+/**
+ * Shared error UI for route-level error.tsx boundaries.
+ * Matches the visual style of the 404 page.
+ */
+export default function ErrorDisplay({
+ title = 'Something went wrong',
+ message = 'An unexpected error occurred. Please try again or go back to the dashboard.',
+ onRetry,
+ onGoHome = true,
+}: ErrorDisplayProps) {
+ return (
+
+
+
+
+
+
+
+ {title}
+
+
+ {message}
+
+
+
+
+
+ )
+}