diff --git a/.changeset/three-cheetahs-lick.md b/.changeset/three-cheetahs-lick.md
new file mode 100644
index 00000000000..3a6d06828e3
--- /dev/null
+++ b/.changeset/three-cheetahs-lick.md
@@ -0,0 +1,7 @@
+---
+"@remix-run/dev": patch
+"@remix-run/react": patch
+"@remix-run/server-runtime": patch
+---
+
+Deprecate `CatchBoundary` in favor of `future.v2_errorBoundary`
diff --git a/docs/route/catch-boundary.md b/docs/route/catch-boundary.md
index bd5985a8d58..556fdc64d9b 100644
--- a/docs/route/catch-boundary.md
+++ b/docs/route/catch-boundary.md
@@ -4,6 +4,8 @@ title: CatchBoundary
# `CatchBoundary`
+The separation of `CatchBoundary` and `ErrorBoundary` has been deprecated and Remix v2 will use a singular `ErrorBoundary` for all thrown Responses and Errors. It is recommended that you opt-into the new behavior in Remix v1 via the `future.v2_errorBoundary` flag in your `remix.config.js` file. Please refer to the [ErrorBoundary (v2)][error-boundary-v2] docs for more information.
+
A `CatchBoundary` is a React component that renders whenever an action or loader throws a `Response`.
**Note:** We use the word "catch" to represent the codepath taken when a `Response` type is thrown; you thought about bailing from the "happy path". This is different from an uncaught error you did not expect to occur.
@@ -29,3 +31,5 @@ export function CatchBoundary() {
);
}
```
+
+[error-boundary-v2]: ./error-boundary-v2
diff --git a/docs/route/error-boundary-v2.md b/docs/route/error-boundary-v2.md
new file mode 100644
index 00000000000..c1a03390587
--- /dev/null
+++ b/docs/route/error-boundary-v2.md
@@ -0,0 +1,11 @@
+---
+title: ErrorBoundary (v2)
+---
+
+# `ErrorBoundary (v2)`
+
+You can opt into the Remix v2 `ErrorBoundary` behavior via the `future.v2_errorBoundary` flag in your `remix.config.js`
+
+If you export an `ErrorBoundary` component from your route module, it will be used as the React Router [`errorElement`][rr-error-element] and will render if you throw from a loader/action or if React throws during rendering your Route component.
+
+[rr-error-element]: https://reactrouter.com/route/error-element
diff --git a/docs/route/error-boundary.md b/docs/route/error-boundary.md
index 8266833e628..5b8e9118a0d 100644
--- a/docs/route/error-boundary.md
+++ b/docs/route/error-boundary.md
@@ -4,6 +4,8 @@ title: ErrorBoundary
# `ErrorBoundary`
+The separation of `CatchBoundary` and `ErrorBoundary` has been deprecated and Remix v2 will use a singular `ErrorBoundary` for all thrown Responses and Errors. It is recommended that you opt-into the new behavior in Remix v1 via the `future.v2_errorBoundary` flag in your `remix.config.js` file. Please refer to the [ErrorBoundary (v2)][error-boundary-v2] docs for more information.
+
An `ErrorBoundary` is a React component that renders whenever there is an error anywhere on the route, either during rendering or during data loading.
**Note:** We use the word "error" to mean an uncaught exception; something you didn't anticipate happening. This is different from other types of "errors" that you are able to recover from easily, for example a 404 error where you can still show something in the user interface to indicate you weren't able to find some data.
@@ -26,3 +28,4 @@ export function ErrorBoundary({ error }) {
```
[error-boundaries]: https://reactjs.org/docs/error-boundaries.html
+[error-boundary-v2]: ./error-boundary-v2
diff --git a/integration/action-test.ts b/integration/action-test.ts
index f8b25e21768..dee315b2a55 100644
--- a/integration/action-test.ts
+++ b/integration/action-test.ts
@@ -17,7 +17,10 @@ test.describe("actions", () => {
test.beforeAll(async () => {
fixture = await createFixture({
- future: { v2_routeConvention: true },
+ future: {
+ v2_routeConvention: true,
+ v2_errorBoundary: true,
+ },
files: {
"app/routes/urlencoded.jsx": js`
import { Form, useActionData } from "@remix-run/react";
diff --git a/integration/defer-test.ts b/integration/defer-test.ts
index df06225a6ca..edac58789e1 100644
--- a/integration/defer-test.ts
+++ b/integration/defer-test.ts
@@ -33,11 +33,10 @@ test.describe("non-aborted", () => {
test.beforeAll(async () => {
fixture = await createFixture({
- future: { v2_routeConvention: true },
- ////////////////////////////////////////////////////////////////////////////
- // 💿 Next, add files to this object, just like files in a real app,
- // `createFixture` will make an app and run your tests against it.
- ////////////////////////////////////////////////////////////////////////////
+ future: {
+ v2_routeConvention: true,
+ v2_errorBoundary: true,
+ },
files: {
"app/components/counter.tsx": js`
import { useState } from "react";
diff --git a/integration/hmr-test.ts b/integration/hmr-test.ts
index ba8a5ed1d1e..13af69b0261 100644
--- a/integration/hmr-test.ts
+++ b/integration/hmr-test.ts
@@ -15,6 +15,7 @@ let fixture = (options: { port: number; appServerPort: number }) => ({
},
unstable_tailwind: true,
v2_routeConvention: true,
+ v2_errorBoundary: true,
},
files: {
"package.json": json({
diff --git a/packages/remix-dev/__tests__/create-test.ts b/packages/remix-dev/__tests__/create-test.ts
index 4508ac91907..2f9eb196b83 100644
--- a/packages/remix-dev/__tests__/create-test.ts
+++ b/packages/remix-dev/__tests__/create-test.ts
@@ -8,7 +8,7 @@ import stripAnsi from "strip-ansi";
import { run } from "../cli/run";
import { server } from "./msw";
-import { flatRoutesWarning } from "../config";
+import { errorBoundaryWarning, flatRoutesWarning } from "../config";
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterAll(() => server.close());
@@ -348,7 +348,9 @@ describe("the create command", () => {
"--no-typescript",
]);
expect(output.trim()).toBe(
- flatRoutesWarning +
+ errorBoundaryWarning +
+ "\n" +
+ flatRoutesWarning +
"\n\n" +
getOptOutOfInstallMessage() +
"\n\n" +
diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts
index 8686777438a..6770b8e66fd 100644
--- a/packages/remix-dev/config.ts
+++ b/packages/remix-dev/config.ts
@@ -397,6 +397,10 @@ export async function readConfig(
}
}
+ if (!appConfig.future?.v2_errorBoundary) {
+ warnOnce(errorBoundaryWarning, "v2_errorBoundary");
+ }
+
if (appConfig.serverBuildTarget) {
warnOnce(serverBuildTargetWarning, "v2_serverBuildTarget");
}
@@ -734,3 +738,11 @@ let listFormat = new Intl.ListFormat("en", {
export let serverBuildTargetWarning = `⚠️ DEPRECATED: The "serverBuildTarget" config option is deprecated. Use a combination of "publicPath", "serverBuildPath", "serverConditions", "serverDependenciesToBundle", "serverMainFields", "serverMinify", "serverModuleFormat" and/or "serverPlatform" instead.`;
export let flatRoutesWarning = `⚠️ DEPRECATED: The old nested folders route convention has been deprecated in favor of "flat routes". Please enable the new routing convention via the \`future.v2_routeConvention\` flag in your \`remix.config.js\` file. For more information, please see https://remix.run/docs/en/main/file-conventions/route-files-v2.`;
+
+export const errorBoundaryWarning =
+ "⚠️ DEPRECATED: The separation of `CatchBoundary` and `ErrorBoundary` has " +
+ "been deprecated and Remix v2 will use a singular `ErrorBoundary` for " +
+ "all thrown values (`Response` and `Error`). Please migrate to the new " +
+ "behavior in Remix v1 via the `future.v2_errorBoundary` flag in your " +
+ "`remix.config.js` file. For more information, see " +
+ "https://remix.run/docs/route/error-boundary-v2";
diff --git a/packages/remix-react/browser.tsx b/packages/remix-react/browser.tsx
index fb3d1f153f9..70eeb7095d5 100644
--- a/packages/remix-react/browser.tsx
+++ b/packages/remix-react/browser.tsx
@@ -14,6 +14,7 @@ import {
import { deserializeErrors } from "./errors";
import type { RouteModules } from "./routeModules";
import { createClientRoutes } from "./routes";
+import { warnOnce } from "./warnings";
/* eslint-disable prefer-let/prefer-let */
declare global {
@@ -138,6 +139,18 @@ if (import.meta && import.meta.hot) {
*/
export function RemixBrowser(_props: RemixBrowserProps): ReactElement {
if (!router) {
+ if (!window.__remixContext.future.v2_errorBoundary) {
+ warnOnce(
+ false,
+ "⚠️ DEPRECATED: The separation of `CatchBoundary` and `ErrorBoundary` has " +
+ "been deprecated and Remix v2 will use a singular `ErrorBoundary` for " +
+ "all thrown values (`Response` and `Error`). Please migrate to the new " +
+ "behavior in Remix v1 via the `future.v2_errorBoundary` flag in your " +
+ "`remix.config.js` file. For more information, see " +
+ "https://remix.run/docs/route/error-boundary-v2"
+ );
+ }
+
let routes = createClientRoutes(
window.__remixManifest.routes,
window.__remixRouteModules,
diff --git a/packages/remix-react/errorBoundaries.tsx b/packages/remix-react/errorBoundaries.tsx
index faa50dd4193..5e083af4d36 100644
--- a/packages/remix-react/errorBoundaries.tsx
+++ b/packages/remix-react/errorBoundaries.tsx
@@ -133,6 +133,8 @@ let RemixCatchContext = React.createContext(
/**
* Returns the status code and thrown response data.
*
+ * @deprecated Please enable the v2_errorBoundary flag
+ *
* @see https://remix.run/route/catch-boundary
*/
export function useCatch<
diff --git a/packages/remix-react/routeModules.ts b/packages/remix-react/routeModules.ts
index 785add903db..147d799bdff 100644
--- a/packages/remix-react/routeModules.ts
+++ b/packages/remix-react/routeModules.ts
@@ -32,6 +32,8 @@ export interface RouteModule {
/**
* A React component that is rendered when the server throws a Response.
*
+ * @deprecated Please enable the v2_errorBoundary flag
+ *
* @see https://remix.run/route/catch-boundary
*/
export type CatchBoundaryComponent = ComponentType<{}>;
@@ -39,6 +41,8 @@ export type CatchBoundaryComponent = ComponentType<{}>;
/**
* A React component that is rendered when there is an error on a route.
*
+ * @deprecated Please enable the v2_errorBoundary flag
+ *
* @see https://remix.run/route/error-boundary
*/
export type ErrorBoundaryComponent = ComponentType<{ error: Error }>;
diff --git a/packages/remix-server-runtime/routeModules.ts b/packages/remix-server-runtime/routeModules.ts
index 380b2549d1f..25a7bb19820 100644
--- a/packages/remix-server-runtime/routeModules.ts
+++ b/packages/remix-server-runtime/routeModules.ts
@@ -36,14 +36,25 @@ export interface ActionFunction {
/**
* A React component that is rendered when the server throws a Response.
+ *
+ * @deprecated Please enable the v2_errorBoundary flag
*/
export type CatchBoundaryComponent = ComponentType;
/**
* A React component that is rendered when there is an error on a route.
+ *
+ * @deprecated Please enable the v2_errorBoundary flag
*/
export type ErrorBoundaryComponent = ComponentType<{ error: Error }>;
+/**
+ * V2 version of the ErrorBoundary that eliminates the distinction between
+ * Error and Catch Boundaries and behaves like RR 6.4 errorElement and captures
+ * errors with useRouteError()
+ */
+export type V2_ErrorBoundaryComponent = ComponentType;
+
/**
* A function that returns HTTP headers to be used for a route. These headers
* will be merged with (and take precedence over) headers from parent routes.
@@ -224,7 +235,7 @@ export type RouteHandle = any;
export interface EntryRouteModule {
CatchBoundary?: CatchBoundaryComponent;
- ErrorBoundary?: ErrorBoundaryComponent;
+ ErrorBoundary?: ErrorBoundaryComponent | V2_ErrorBoundaryComponent;
default: RouteComponent;
handle?: RouteHandle;
links?: LinksFunction;