diff --git a/src/core/ui/Widgets/PageErrorBoundary.test.tsx b/src/core/ui/Widgets/PageErrorBoundary.test.tsx
new file mode 100644
index 000000000..e4d703dd6
--- /dev/null
+++ b/src/core/ui/Widgets/PageErrorBoundary.test.tsx
@@ -0,0 +1,25 @@
+import { render, screen } from "@testing-library/react";
+import { MemoryRouter } from "react-router-dom";
+import { describe, expect, test, vi } from "vitest";
+import { RouteErrorBoundary } from "./PageErrorBoundary";
+
+const FailedPage = () => {
+ throw new Error("Request failed with status code 403");
+};
+
+describe("RouteErrorBoundary", () => {
+ test("shows an informative message when a page fails to render", () => {
+ vi.spyOn(console, "error").mockImplementation(() => undefined);
+
+ render(
+
+
+
+
+
+ );
+
+ expect(screen.getByText("Unable to load this page")).toBeInTheDocument();
+ expect(screen.getByText("Request failed with status code 403")).toBeInTheDocument();
+ });
+});
diff --git a/src/core/ui/Widgets/PageErrorBoundary.tsx b/src/core/ui/Widgets/PageErrorBoundary.tsx
new file mode 100644
index 000000000..321439c5e
--- /dev/null
+++ b/src/core/ui/Widgets/PageErrorBoundary.tsx
@@ -0,0 +1,44 @@
+import { Alert, AlertTitle, Container } from "@mui/material";
+import React, { ErrorInfo, ReactNode } from "react";
+import { useLocation } from "react-router-dom";
+
+type PageErrorBoundaryProps = {
+ children: ReactNode;
+};
+
+type PageErrorBoundaryState = {
+ error: Error | null;
+};
+
+class PageErrorBoundary extends React.Component {
+ state: PageErrorBoundaryState = { error: null };
+
+ static getDerivedStateFromError(error: Error): PageErrorBoundaryState {
+ return { error };
+ }
+
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
+ console.error("Unable to render page", error, errorInfo);
+ }
+
+ render() {
+ if (this.state.error) {
+ return (
+
+
+ Unable to load this page
+ {this.state.error.message}
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
+
+export const RouteErrorBoundary = ({ children }: PageErrorBoundaryProps) => {
+ const location = useLocation();
+
+ return {children};
+};
diff --git a/src/index.tsx b/src/index.tsx
index bb8ee7c4f..b54429e39 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -23,6 +23,9 @@ const queryClient = new QueryClient({
defaultOptions: {
queries: {
+ // Let the route error boundary replace otherwise blank pages when an
+ // initial request fails (for example, with a 403 response).
+ throwOnError: true,
// Don't retry client errors (e.g. a 404 for a deleted/stale resource), they
// won't succeed on a retry and only delay surfacing the result
retry: (failureCount, error) => {
diff --git a/src/routes.tsx b/src/routes.tsx
index f960002e6..7c872483c 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -31,6 +31,7 @@ import {
WeightOverview,
} from "@/pages";
import { ExerciseDetailPage } from "@/pages/ExerciseDetails";
+import { RouteErrorBoundary } from "@/core/ui/Widgets/PageErrorBoundary";
import React from "react";
import { Route, Routes } from "react-router-dom";
@@ -42,7 +43,8 @@ import { Route, Routes } from "react-router-dom";
*/
export const WgerRoutes = () => {
return (
-
+
+
} />
@@ -138,6 +140,7 @@ export const WgerRoutes = () => {
}
/>
-
+
+
);
};