diff --git a/apps/journal/app/root.tsx b/apps/journal/app/root.tsx index ca53230..f68979f 100644 --- a/apps/journal/app/root.tsx +++ b/apps/journal/app/root.tsx @@ -1,5 +1,6 @@ -import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; +import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from "react-router"; import type { LinksFunction } from "react-router"; +import type { Route } from "./+types/root"; import stylesheet from "@trails-cool/ui/styles.css?url"; export const links: LinksFunction = () => [{ rel: "stylesheet", href: stylesheet }]; @@ -25,3 +26,33 @@ export function Layout({ children }: { children: React.ReactNode }) { export default function App() { return ; } + +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + if (isRouteErrorResponse(error)) { + return ( +
+

{error.status}

+

+ {error.status === 404 && "Page not found"} + {error.status === 503 && "Service temporarily unavailable. Please try again later."} + {error.status !== 404 && error.status !== 503 && (error.statusText || "Something went wrong")} +

+ + Go home + +
+ ); + } + + return ( +
+

Error

+

+ {error instanceof Error ? error.message : "An unexpected error occurred"} +

+ + Go home + +
+ ); +} diff --git a/apps/planner/app/root.tsx b/apps/planner/app/root.tsx index 17b1eb6..bf39ded 100644 --- a/apps/planner/app/root.tsx +++ b/apps/planner/app/root.tsx @@ -1,5 +1,6 @@ -import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; +import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from "react-router"; import type { LinksFunction } from "react-router"; +import type { Route } from "./+types/root"; import stylesheet from "@trails-cool/ui/styles.css?url"; export const links: LinksFunction = () => [{ rel: "stylesheet", href: stylesheet }]; @@ -25,3 +26,31 @@ export function Layout({ children }: { children: React.ReactNode }) { export default function App() { return ; } + +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + if (isRouteErrorResponse(error)) { + return ( +
+
+

{error.status}

+

+ {error.status === 404 && "Page not found"} + {error.status === 503 && "Service temporarily unavailable"} + {error.status !== 404 && error.status !== 503 && (error.statusText || "Something went wrong")} +

+
+
+ ); + } + + return ( +
+
+

Error

+

+ {error instanceof Error ? error.message : "An unexpected error occurred"} +

+
+
+ ); +} diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index df18f2e..26f84fa 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -20,23 +20,22 @@ export type Database = ReturnType; */ export function withDb(handler: () => Promise): Promise { return handler().catch((error) => { - // Re-throw React Router responses (redirects, data() throws) - // Check for Response, ErrorResponseImpl, or anything with a status property + // Re-throw anything that looks like a React Router response: + // - Response (redirects) + // - ErrorResponseImpl from data() throws (has status + data) if ( error instanceof Response || - (error && typeof error === "object" && "status" in error && "data" in error) + (error != null && typeof error === "object" && "status" in error) ) { throw error; } - // Any other error from a DB-wrapped handler is treated as DB unavailable + // Database error — throw as a 503 that the error boundary will catch const message = error instanceof Error ? error.message : String(error); console.error("[withDb] Database error:", message); - throw new Response(JSON.stringify({ error: "Database unavailable" }), { - status: 503, - headers: { "Content-Type": "application/json" }, - }); + // Use the same shape as data() throw so isRouteErrorResponse works + throw new Response("Database unavailable", { status: 503, statusText: "Service Unavailable" }); }); }