From ec389417acd76559000ad548d38c7ab024383fd4 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 15 Jan 2026 17:16:59 -0500 Subject: [PATCH 1/3] Fix error type (Error -> unknown) --- README.md | 2 +- lib/components/ErrorBoundary.test.tsx | 17 +++++++++-------- lib/components/ErrorBoundary.tsx | 4 ++-- lib/context/ErrorBoundaryContext.ts | 2 +- lib/hooks/useErrorBoundary.test.tsx | 5 ++++- lib/hooks/useErrorBoundary.ts | 12 ++++++------ lib/index.ts | 1 + lib/types.ts | 8 ++++---- lib/utils/getErrorMessage.ts | 17 +++++++++++++++++ package.json | 2 +- public/generated/docs/ErrorBoundary.json | 4 ++-- public/generated/examples/ErrorLogging.json | 2 +- .../generated/examples/FallbackComponent.json | 2 +- public/generated/examples/RenderProp.json | 2 +- src/routes/examples/ErrorLogging.tsx | 2 +- src/routes/examples/FallbackComponent.tsx | 4 ++-- src/routes/examples/RenderProp.tsx | 4 ++-- vitest.config.ts | 2 +- 18 files changed, 57 insertions(+), 35 deletions(-) create mode 100644 lib/utils/getErrorMessage.ts diff --git a/README.md b/README.md index e65354fc..a470182f 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ None onError

Optional callback to enable e.g. logging error information to a server. -@param error Error that was thrown +@param error Value that was thrown; typically an instance of Error @param info React "component stack" identifying where the error was thrown

diff --git a/lib/components/ErrorBoundary.test.tsx b/lib/components/ErrorBoundary.test.tsx index 8538fa2c..31cc9c53 100644 --- a/lib/components/ErrorBoundary.test.tsx +++ b/lib/components/ErrorBoundary.test.tsx @@ -16,6 +16,7 @@ import type { FallbackProps, OnErrorCallback, } from "../types"; +import { getErrorMessage } from "../utils/getErrorMessage"; describe("ErrorBoundary", () => { let container: HTMLDivElement; @@ -84,7 +85,7 @@ describe("ErrorBoundary", () => { render({ onError }); expect(onError).toHaveBeenCalledTimes(1); - expect(onError.mock.calls[0][0].message).toEqual("💥💥💥"); + expect(getErrorMessage(onError.mock.calls[0][0])).toEqual("💥💥💥"); }); it('should call "onReset" when boundary reset via imperative API', () => { @@ -169,7 +170,7 @@ describe("ErrorBoundary", () => { describe('"FallbackComponent"', () => { let fallbackComponent: Mock<(props: FallbackProps) => ReactElement>; - let lastRenderedError: Error | null = null; + let lastRenderedError: unknown | null = null; let lastRenderedResetErrorBoundary: | FallbackProps["resetErrorBoundary"] | null = null; @@ -204,7 +205,7 @@ describe("ErrorBoundary", () => { it("should render fallback in the event of an error", () => { shouldThrow = true; render(); - expect(lastRenderedError?.message).toBe("💥💥💥"); + expect(getErrorMessage(lastRenderedError)).toBe("💥💥💥"); expect(container.textContent).toBe("FallbackComponent"); }); @@ -235,7 +236,7 @@ describe("ErrorBoundary", () => { }); describe('"fallbackRender" render prop', () => { - let lastRenderedError: Error | null = null; + let lastRenderedError: unknown | null = null; let lastRenderedResetErrorBoundary: | FallbackProps["resetErrorBoundary"] | null = null; @@ -271,7 +272,7 @@ describe("ErrorBoundary", () => { it("should render fallback in the event of an error", () => { shouldThrow = true; render(); - expect(lastRenderedError?.message).toBe("💥💥💥"); + expect(getErrorMessage(lastRenderedError)).toBe("💥💥💥"); expect(fallbackRender).toHaveBeenCalled(); expect(container.textContent).toBe("fallbackRender"); }); @@ -279,7 +280,7 @@ describe("ErrorBoundary", () => { it("should re-render children if boundary is reset via prop", () => { shouldThrow = true; render(); - expect(lastRenderedError?.message).toBe("💥💥💥"); + expect(getErrorMessage(lastRenderedError)).toBe("💥💥💥"); expect(fallbackRender).toHaveBeenCalled(); expect(container.textContent).toBe("fallbackRender"); @@ -295,7 +296,7 @@ describe("ErrorBoundary", () => { it("should re-render children if boundary is reset reset keys", () => { shouldThrow = true; render({ resetKeys: [1] }); - expect(lastRenderedError?.message).toBe("💥💥💥"); + expect(getErrorMessage(lastRenderedError)).toBe("💥💥💥"); expect(fallbackRender).toHaveBeenCalled(); expect(container.textContent).toBe("fallbackRender"); @@ -306,7 +307,7 @@ describe("ErrorBoundary", () => { }); describe("thrown values", () => { - let lastRenderedError: Error | null = null; + let lastRenderedError: unknown | null = null; let fallbackRender: (props: FallbackProps) => ReactElement; let onError: Mock<(...args: unknown[]) => unknown>; diff --git a/lib/components/ErrorBoundary.tsx b/lib/components/ErrorBoundary.tsx index 90e9da51..5943ea95 100644 --- a/lib/components/ErrorBoundary.tsx +++ b/lib/components/ErrorBoundary.tsx @@ -7,7 +7,7 @@ const isDevelopment = import.meta.env.DEV; type ErrorBoundaryState = | { didCatch: true; - error: Error; + error: unknown; } | { didCatch: false; @@ -63,7 +63,7 @@ export class ErrorBoundary extends Component< } } - componentDidCatch(error: Error, info: ErrorInfo) { + componentDidCatch(error: unknown, info: ErrorInfo) { this.props.onError?.(error, info); } diff --git a/lib/context/ErrorBoundaryContext.ts b/lib/context/ErrorBoundaryContext.ts index 6ec98c53..205aec12 100644 --- a/lib/context/ErrorBoundaryContext.ts +++ b/lib/context/ErrorBoundaryContext.ts @@ -2,7 +2,7 @@ import { createContext } from "react"; export type ErrorBoundaryContextType = { didCatch: boolean; - error: Error | null; + error: unknown | null; resetErrorBoundary: (...args: unknown[]) => void; }; diff --git a/lib/hooks/useErrorBoundary.test.tsx b/lib/hooks/useErrorBoundary.test.tsx index ca88a672..ffb0ad9d 100644 --- a/lib/hooks/useErrorBoundary.test.tsx +++ b/lib/hooks/useErrorBoundary.test.tsx @@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { ErrorBoundary } from "../components/ErrorBoundary"; import { assert } from "../utils/assert"; +import { getErrorMessage } from "../utils/getErrorMessage"; import { useErrorBoundary, type UseErrorBoundaryApi } from "./useErrorBoundary"; describe("useErrorBoundary", () => { @@ -41,7 +42,9 @@ describe("useErrorBoundary", () => { render(
Fallback: {error.message}
} + fallbackRender={({ error }) => ( +
Fallback: {getErrorMessage(error)}
+ )} >
, diff --git a/lib/hooks/useErrorBoundary.ts b/lib/hooks/useErrorBoundary.ts index d86c3a5d..869431b7 100644 --- a/lib/hooks/useErrorBoundary.ts +++ b/lib/hooks/useErrorBoundary.ts @@ -3,13 +3,13 @@ import { ErrorBoundaryContext } from "../context/ErrorBoundaryContext"; import { assertErrorBoundaryContext } from "../utils/assertErrorBoundaryContext"; type UseErrorBoundaryState = - | { error: Error; hasError: true } + | { error: unknown; hasError: true } | { error: null; hasError: false }; export type UseErrorBoundaryApi = { - error: Error | null; + error: unknown | null; resetBoundary: () => void; - showBoundary: (error: Error) => void; + showBoundary: (error: unknown) => void; }; /** @@ -21,7 +21,7 @@ export function useErrorBoundary(): { /** * The currently visible `Error` (if one has been thrown). */ - error: Error | null; + error: unknown | null; /** * Method to reset and retry the nearest active error boundary (if one is active). @@ -35,7 +35,7 @@ export function useErrorBoundary(): { * Errors thrown in event handlers, or after async code has run, will not be caught. * This method is a way to imperatively trigger an error boundary during these phases. */ - showBoundary: (error: Error) => void; + showBoundary: (error: unknown) => void; } { const context = useContext(ErrorBoundaryContext); @@ -55,7 +55,7 @@ export function useErrorBoundary(): { resetErrorBoundary(); setState({ error: null, hasError: false }); }, - showBoundary: (error: Error) => + showBoundary: (error: unknown) => setState({ error, hasError: true, diff --git a/lib/index.ts b/lib/index.ts index b3739d7f..8da9b985 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -3,6 +3,7 @@ export { ErrorBoundary } from "./components/ErrorBoundary"; export { ErrorBoundaryContext } from "./context/ErrorBoundaryContext"; export { useErrorBoundary } from "./hooks/useErrorBoundary"; +export { getErrorMessage } from "./utils/getErrorMessage"; export { withErrorBoundary } from "./utils/withErrorBoundary"; export type { ErrorBoundaryContextType } from "./context/ErrorBoundaryContext"; diff --git a/lib/types.ts b/lib/types.ts index 1b72a321..592036c2 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -6,20 +6,20 @@ import type { } from "react"; export type FallbackProps = { - error: Error; + error: unknown; resetErrorBoundary: (...args: unknown[]) => void; }; -export type OnErrorCallback = (error: Error, info: ErrorInfo) => void; +export type OnErrorCallback = (error: unknown, info: ErrorInfo) => void; type ErrorBoundarySharedProps = PropsWithChildren<{ /** * Optional callback to enable e.g. logging error information to a server. * - * @param error Error that was thrown + * @param error Value that was thrown; typically an instance of `Error` * @param info React "component stack" identifying where the error was thrown */ - onError?: (error: Error, info: ErrorInfo) => void; + onError?: (error: unknown, info: ErrorInfo) => void; /** * Optional callback to to be notified when an error boundary is "reset" so React can retry the failed render. diff --git a/lib/utils/getErrorMessage.ts b/lib/utils/getErrorMessage.ts new file mode 100644 index 00000000..f23a1dbc --- /dev/null +++ b/lib/utils/getErrorMessage.ts @@ -0,0 +1,17 @@ +export function getErrorMessage(thrown: unknown): string | undefined { + switch (typeof thrown) { + case "object": { + if ( + thrown !== null && + "message" in thrown && + typeof thrown.message === "string" + ) { + return thrown.message; + } + break; + } + case "string": { + return thrown; + } + } +} diff --git a/package.json b/package.json index d002f47a..b26b5da3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-error-boundary", - "version": "6.0.3", + "version": "6.0.4", "type": "module", "description": "Simple reusable React error boundary component", "author": "Brian Vaughn ", diff --git a/public/generated/docs/ErrorBoundary.json b/public/generated/docs/ErrorBoundary.json index 86b8ea53..59d19a46 100644 --- a/public/generated/docs/ErrorBoundary.json +++ b/public/generated/docs/ErrorBoundary.json @@ -21,10 +21,10 @@ "onError": { "description": [ { - "content": "

Optional callback to enable e.g. logging error information to a server.

\n\n" + "content": "

Optional callback to enable e.g. logging error information to a server.

\n\n" } ], - "html": "
onError?: (error: Error, info: ErrorInfo) => void
", + "html": "
onError?: (error: unknown, info: ErrorInfo) => void
", "name": "onError", "required": false }, diff --git a/public/generated/examples/ErrorLogging.json b/public/generated/examples/ErrorLogging.json index fafc04c9..ccdba707 100644 --- a/public/generated/examples/ErrorLogging.json +++ b/public/generated/examples/ErrorLogging.json @@ -1,3 +1,3 @@ { - "html": "
import type { ErrorInfo } from \"react\";
\n
import { ErrorBoundary } from \"react-error-boundary\";
\n
 
\n
function logError(error: Error, info: ErrorInfo) {
\n
// Do something with the error, e.g. log to an external API
\n
}
\n
 
\n
<ErrorBoundary FallbackComponent={ErrorFallback} onError={logError}>
\n
<YourApplication />
\n
</ErrorBoundary>;
" + "html": "
import type { ErrorInfo } from \"react\";
\n
import { ErrorBoundary } from \"react-error-boundary\";
\n
 
\n
function logError(error: unknown, info: ErrorInfo) {
\n
// Do something with the error, e.g. log to an external API
\n
}
\n
 
\n
<ErrorBoundary FallbackComponent={ErrorFallback} onError={logError}>
\n
<YourApplication />
\n
</ErrorBoundary>;
" } \ No newline at end of file diff --git a/public/generated/examples/FallbackComponent.json b/public/generated/examples/FallbackComponent.json index a45096a5..78f935e6 100644 --- a/public/generated/examples/FallbackComponent.json +++ b/public/generated/examples/FallbackComponent.json @@ -1,3 +1,3 @@ { - "html": "
import { ErrorBoundary, type FallbackProps } from \"react-error-boundary\";
\n
 
\n
function Fallback({ error, resetErrorBoundary }: FallbackProps) {
\n
return (
\n
<div role=\"alert\">
\n
<p>Something went wrong:</p>
\n
<pre style={{ color: \"red\" }}>{error.message}</pre>
\n
<button onClick={resetErrorBoundary}>Retry</button>
\n
</div>
\n
);
\n
}
\n
 
\n
<ErrorBoundary
\n
FallbackComponent={Fallback}
\n
onReset={(details) => {
\n
// Reset the state of your app so the error doesn't happen again
\n
}}
\n
>
\n
<YourApplication />
\n
</ErrorBoundary>;
" + "html": "
import { ErrorBoundary, getErrorMessage, type FallbackProps } from \"react-error-boundary\";
\n
 
\n
function Fallback({ error, resetErrorBoundary }: FallbackProps) {
\n
return (
\n
<div role=\"alert\">
\n
<p>Something went wrong:</p>
\n
<pre style={{ color: \"red\" }}>{getErrorMessage(error)}</pre>
\n
<button onClick={resetErrorBoundary}>Retry</button>
\n
</div>
\n
);
\n
}
\n
 
\n
<ErrorBoundary
\n
FallbackComponent={Fallback}
\n
onReset={(details) => {
\n
// Reset the state of your app so the error doesn't happen again
\n
}}
\n
>
\n
<YourApplication />
\n
</ErrorBoundary>;
" } \ No newline at end of file diff --git a/public/generated/examples/RenderProp.json b/public/generated/examples/RenderProp.json index ca15c15e..25ef1b36 100644 --- a/public/generated/examples/RenderProp.json +++ b/public/generated/examples/RenderProp.json @@ -1,3 +1,3 @@ { - "html": "
import { ErrorBoundary } from \"react-error-boundary\";
\n
 
\n
<ErrorBoundary
\n
fallbackRender={({ error, resetErrorBoundary }) => (
\n
<div role=\"alert\">
\n
<p>Something went wrong:</p>
\n
<pre style={{ color: \"red\" }}>{error.message}</pre>
\n
<button onClick={resetErrorBoundary}>Retry</button>
\n
</div>
\n
)}
\n
onReset={(details) => {
\n
// Reset the state of your app so the error doesn't happen again
\n
}}
\n
>
\n
<YourApplication />
\n
</ErrorBoundary>;
" + "html": "
import { ErrorBoundary, getErrorMessage } from \"react-error-boundary\";
\n
 
\n
<ErrorBoundary
\n
fallbackRender={({ error, resetErrorBoundary }) => (
\n
<div role=\"alert\">
\n
<p>Something went wrong:</p>
\n
<pre style={{ color: \"red\" }}>{getErrorMessage(error)}</pre>
\n
<button onClick={resetErrorBoundary}>Retry</button>
\n
</div>
\n
)}
\n
onReset={(details) => {
\n
// Reset the state of your app so the error doesn't happen again
\n
}}
\n
>
\n
<YourApplication />
\n
</ErrorBoundary>;
" } \ No newline at end of file diff --git a/src/routes/examples/ErrorLogging.tsx b/src/routes/examples/ErrorLogging.tsx index 4e765d57..d26df3f4 100644 --- a/src/routes/examples/ErrorLogging.tsx +++ b/src/routes/examples/ErrorLogging.tsx @@ -1,7 +1,7 @@ import type { ErrorInfo } from "react"; import { ErrorBoundary } from "react-error-boundary"; -function logError(error: Error, info: ErrorInfo) { +function logError(error: unknown, info: ErrorInfo) { // Do something with the error, e.g. log to an external API error; // hidden info; // hidden diff --git a/src/routes/examples/FallbackComponent.tsx b/src/routes/examples/FallbackComponent.tsx index 594a6c53..30ea517b 100644 --- a/src/routes/examples/FallbackComponent.tsx +++ b/src/routes/examples/FallbackComponent.tsx @@ -1,10 +1,10 @@ -import { ErrorBoundary, type FallbackProps } from "react-error-boundary"; +import { ErrorBoundary, getErrorMessage, type FallbackProps } from "react-error-boundary"; function Fallback({ error, resetErrorBoundary }: FallbackProps) { return (

Something went wrong:

-
{error.message}
+
{getErrorMessage(error)}
); diff --git a/src/routes/examples/RenderProp.tsx b/src/routes/examples/RenderProp.tsx index c3d4c0ca..7a6fa74e 100644 --- a/src/routes/examples/RenderProp.tsx +++ b/src/routes/examples/RenderProp.tsx @@ -1,10 +1,10 @@ -import { ErrorBoundary } from "react-error-boundary"; +import { ErrorBoundary, getErrorMessage } from "react-error-boundary"; (

Something went wrong:

-
{error.message}
+
{getErrorMessage(error)}
)} diff --git a/vitest.config.ts b/vitest.config.ts index b1761030..64885ff8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -17,5 +17,5 @@ export default mergeConfig( setupFiles: "./vitest.setup.js", exclude: ["node_modules", "integrations"], }, - }) + }), ); From cf6f1479628f74aeb8e18c40a2aeb34c0fbbd2f3 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 15 Jan 2026 17:25:16 -0500 Subject: [PATCH 2/3] Update docs --- .../generated/examples/GetErrorMessage.json | 3 +++ src/App.tsx | 3 +++ src/routes.ts | 1 + src/routes/GetErrorMessageRoute.tsx | 24 +++++++++++++++++++ src/routes/examples/GetErrorMessage.ts | 13 ++++++++++ 5 files changed, 44 insertions(+) create mode 100644 public/generated/examples/GetErrorMessage.json create mode 100644 src/routes/GetErrorMessageRoute.tsx create mode 100644 src/routes/examples/GetErrorMessage.ts diff --git a/public/generated/examples/GetErrorMessage.json b/public/generated/examples/GetErrorMessage.json new file mode 100644 index 00000000..57382300 --- /dev/null +++ b/public/generated/examples/GetErrorMessage.json @@ -0,0 +1,3 @@ +{ + "html": "
import { getErrorMessage, type FallbackProps } from \"react-error-boundary\";
\n
 
\n
function Fallback({ error }: FallbackProps) {
\n
// Because 'error' can be anything, it's safest not to assume it's an Error
\n
// Use the getErrorMessage helper method to extract the message instead.
\n
const message = getErrorMessage(error) ?? \"Unknown error\";
\n
 
\n
// Render fallback UI...
\n
}
" +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 69ac1abd..bf1b26e1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -41,6 +41,9 @@ export default function App() { withErrorBoundary HOC + + getErrorMessage helper + Common questions Support diff --git a/src/routes.ts b/src/routes.ts index 3490f9ef..2d37e19b 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -24,6 +24,7 @@ export const routes = { "/api/with-error-boundary-hoc": lazy( () => import("./routes/WithErrorBoundaryRoute"), ), + "/api/get-error-message": lazy(() => import("./routes/GetErrorMessageRoute")), } satisfies Record; export type Routes = Record; diff --git a/src/routes/GetErrorMessageRoute.tsx b/src/routes/GetErrorMessageRoute.tsx new file mode 100644 index 00000000..e4e4c0bf --- /dev/null +++ b/src/routes/GetErrorMessageRoute.tsx @@ -0,0 +1,24 @@ +import { Box, Code, ExternalLink, Header } from "react-lib-tools"; +import { html } from "../../public/generated/examples/GetErrorMessage.json"; + +export default function GetErrorMessageRoute() { + return ( + +
+
+ Typically thrown errors in JavaScript are instances of type{" "} + Error, but{" "} + + this is not always the case + + . Any value can be thrown- strings, numbers, even null or{" "} + undefined. +
+
+ To simplify working with thrown values, this library exports a utility + method called getErrorMessage. +
+ + + ); +} diff --git a/src/routes/examples/GetErrorMessage.ts b/src/routes/examples/GetErrorMessage.ts new file mode 100644 index 00000000..dcd6fe8e --- /dev/null +++ b/src/routes/examples/GetErrorMessage.ts @@ -0,0 +1,13 @@ +import { getErrorMessage, type FallbackProps } from "react-error-boundary"; + +function Fallback({ error }: FallbackProps) { + // Because 'error' can be anything, it's safest not to assume it's an Error + // Use the getErrorMessage helper method to extract the message instead. + const message = getErrorMessage(error) ?? "Unknown error"; + + // Render fallback UI... +} + +// + +export { Fallback }; From 33d68c27833375d6dd5d08e5a52b7e102614b958 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 15 Jan 2026 17:29:26 -0500 Subject: [PATCH 3/3] Fix TSC --- src/routes/examples/GetErrorMessage.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/examples/GetErrorMessage.ts b/src/routes/examples/GetErrorMessage.ts index dcd6fe8e..57d06a90 100644 --- a/src/routes/examples/GetErrorMessage.ts +++ b/src/routes/examples/GetErrorMessage.ts @@ -6,6 +6,7 @@ function Fallback({ error }: FallbackProps) { const message = getErrorMessage(error) ?? "Unknown error"; // Render fallback UI... + return message; // hidden } //