Skip to content

Error Handling

RedwoodSDK supports React 19’s powerful error handling APIs, allowing you to catch and handle errors at the React root level. This enables production-ready error monitoring, custom recovery strategies, and better debugging capabilities.

React 19 introduced two main error handling APIs:

  • onUncaughtError: Handles uncaught errors that escape error boundaries (async errors, event handler errors, etc.)
  • onCaughtError: Handles errors that are caught by error boundaries

These APIs are available through the hydrateRootOptions parameter in initClient, which passes options directly to React’s hydrateRoot function.

Use onUncaughtError for errors that occur during the React lifecycle but are not caught by error boundaries:

  • Errors during initial hydration or rendering.
  • Errors inside useEffect or other lifecycle hooks.
  • Errors during React transitions.
Example: Uncaught error in lifecycle
"use client";
import { useEffect } from "react";
export function Component() {
useEffect(() => {
// This error will trigger onUncaughtError
throw new Error("Lifecycle error");
}, []);
return <div>Component</div>;
}

Use onCaughtError for errors that are caught by error boundaries:

  • Component rendering errors
  • Errors in component lifecycle methods
  • Errors caught by <ErrorBoundary> components
Example: Error caught by error boundary
"use client";
export function ErrorBoundary({ children }: { children: React.ReactNode }) {
// This error will trigger onCaughtError
return <ErrorBoundary>{children}</ErrorBoundary>;
}
export function Component() {
throw new Error("Component error");
return <div>This won't render</div>;
}
  1. Import initClient from rwsdk/client:
src/client.tsx
import { initClient } from "rwsdk/client";
  1. Configure error handlers via hydrateRootOptions:
src/client.tsx
initClient({
hydrateRootOptions: {
onUncaughtError: (error, errorInfo) => {
console.error("Uncaught error:", error);
console.error("Component stack:", errorInfo.componentStack);
},
onCaughtError: (error, errorInfo) => {
console.error("Caught error:", error);
console.error("Component stack:", errorInfo.componentStack);
},
},
});
  1. The error handlers will now catch and log all React errors in your application.

To ensure that all client-side errors (including event handlers, timeouts, and promise rejections) are caught and handled uniformly, you should combine React’s error handlers with global browser listeners.

This pattern is particularly useful for redirecting users to a dedicated error page on any fatal error:

src/client.tsx
import { initClient } from "rwsdk/client";
const redirectToError = () => {
// Use replace to avoid keeping the broken page in history
window.location.replace("/error");
};
// 1. Catch imperative errors (event handlers, timeouts, etc.)
window.addEventListener("error", (event) => {
console.error("Global error caught:", event.message);
redirectToError();
});
// 2. Catch unhandled promise rejections
window.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled promise rejection:", event.reason);
redirectToError();
});
initClient({
hydrateRootOptions: {
// 3. Catch React-specific uncaught errors (rendering, hydration)
onUncaughtError: (error, errorInfo) => {
console.error("React uncaught error:", error, errorInfo);
redirectToError();
},
// 4. Catch errors caught by error boundaries
onCaughtError: (error, errorInfo) => {
console.error("React caught error:", error, errorInfo);
redirectToError();
},
},
});
src/client.tsx
import { initClient } from "rwsdk/client";
import * as Sentry from "@sentry/browser";
initClient({
hydrateRootOptions: {
onUncaughtError: (error, errorInfo) => {
Sentry.captureException(error, {
contexts: {
react: {
componentStack: errorInfo.componentStack,
errorBoundary: errorInfo.errorBoundary?.constructor.name,
},
},
tags: { errorType: "uncaught" },
});
},
onCaughtError: (error, errorInfo) => {
Sentry.captureException(error, {
contexts: {
react: {
componentStack: errorInfo.componentStack,
errorBoundary: errorInfo.errorBoundary?.constructor.name,
},
},
tags: { errorType: "caught" },
});
},
},
});
src/client.tsx
import { initClient } from "rwsdk/client";
function sendToMonitoring(
error: unknown,
errorInfo: { componentStack: string; errorBoundary?: React.Component | null },
type: "uncaught" | "caught",
) {
fetch("/api/errors", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
componentStack: errorInfo.componentStack,
errorBoundary: errorInfo.errorBoundary?.constructor.name,
type,
timestamp: new Date().toISOString(),
}),
});
}
initClient({
hydrateRootOptions: {
onUncaughtError: (error, errorInfo) => {
sendToMonitoring(error, errorInfo, "uncaught");
},
onCaughtError: (error, errorInfo) => {
sendToMonitoring(error, errorInfo, "caught");
},
},
});
src/client.tsx
import { initClient } from "rwsdk/client";
function showErrorToast(message: string) {
// Your toast implementation
console.log("Error:", message);
}
initClient({
hydrateRootOptions: {
onUncaughtError: (error, errorInfo) => {
// Log for debugging
console.error("Uncaught error:", error, errorInfo);
// Show user-friendly message
showErrorToast("Something went wrong. Please try again.");
// Send to monitoring
sendToMonitoring(error, errorInfo);
},
},
});
src/client.tsx
import { initClient } from "rwsdk/client";
function isCriticalError(error: unknown): boolean {
// Define your critical error logic
return error instanceof Error && error.message.includes("CRITICAL");
}
initClient({
hydrateRootOptions: {
onUncaughtError: (error, errorInfo) => {
console.error("Uncaught error:", error, errorInfo);
if (isCriticalError(error)) {
// Reload page for critical errors
window.location.reload();
} else {
// Handle non-critical errors gracefully
showErrorToast("An error occurred. Please refresh the page.");
}
},
},
});

Even if you’re sending errors to a monitoring service, log them locally for debugging:

onUncaughtError: (error, errorInfo) => {
console.error("Uncaught error:", error);
console.error("Component stack:", errorInfo.componentStack);
// Then send to monitoring
};

The errorInfo.componentStack provides valuable debugging information. Always include it in your error reports:

Sentry.captureException(error, {
contexts: {
react: {
componentStack: errorInfo.componentStack,
},
},
});

Use tags or metadata to distinguish between caught and uncaught errors:

onUncaughtError: (error, errorInfo) => {
sendToMonitoring(error, { ...errorInfo, type: "uncaught" });
},
onCaughtError: (error, errorInfo) => {
sendToMonitoring(error, { ...errorInfo, type: "caught" });
},

Error handlers should not throw errors themselves. Keep them lightweight:

onUncaughtError: (error, errorInfo) => {
try {
// Safe error handling
sendToMonitoring(error, errorInfo);
} catch (e) {
// Fallback to console if monitoring fails
console.error("Error in error handler:", e);
}
},

Error boundaries are React components that catch errors in their child component tree. However, they have important limitations in a React Server Components (RSC) world:

  • Client-only: Error boundaries only work in client components ("use client"), not in server components
  • Forces client components: Using error boundaries requires nesting components inside them, which forces all child components to be client components, defeating the purpose of RSC
  • Limited placement: In RSC architectures, there’s often no good place to wrap server components with error boundaries since they render on the server
  • Post-hydration only: Error boundaries only catch errors after client-side hydration, not during initial server rendering

Root-level error handlers (onUncaughtError and onCaughtError) are more suitable for RSC applications because they:

  • Work for both server-rendered and client-rendered errors (post-hydration)
  • Don’t require wrapping components or converting them to client components
  • Catch errors that escape error boundaries
  • Provide a single place to handle all React errors for monitoring and logging
  • Preserve the benefits of RSC by not forcing components to be client components

For server-side rendering errors, use the router’s error handling (see router error handling documentation).

  • Component rendering errors (post-hydration).
  • Errors inside useEffect or other lifecycle methods.
  • Errors during React transitions.
  • Errors that escape error boundaries.
  • Imperative event handlers: Errors in onClick, onBlur, etc., often bubble directly to the browser.
  • Asynchronous code: setTimeout, setInterval, or third-party callbacks outside of React’s control.
  • Unhandled rejections: Promise failures that are not part of a React transition.
  • Server-side RSC rendering errors: Use router error handling or wrap defineApp’s fetch method.
  • SSR errors: Handled server-side.
src/client.tsx
import { initClient } from "rwsdk/client";
const isDevelopment = import.meta.env.DEV;
initClient({
hydrateRootOptions: {
onUncaughtError: (error, errorInfo) => {
if (isDevelopment) {
// Detailed logging in development
console.error("Uncaught error:", error);
console.error("Component stack:", errorInfo.componentStack);
} else {
// Send to monitoring in production
sendToMonitoring(error, errorInfo);
}
},
},
});
src/client.tsx
import { initClient } from "rwsdk/client";
initClient({
hydrateRootOptions: {
onUncaughtError: (error, errorInfo) => {
// Log error
console.error("Uncaught error:", error, errorInfo);
// Send to monitoring
sendToMonitoring(error, errorInfo);
// Show user feedback
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
showErrorNotification(`Error: ${errorMessage}`);
},
},
});

React 19’s error handling APIs provide powerful tools for monitoring and handling errors in production. By configuring onUncaughtError and onCaughtError through hydrateRootOptions, you can:

  • Track errors in production
  • Integrate with monitoring services
  • Implement custom recovery strategies
  • Improve debugging with component stacks

These root-level error handlers are particularly well-suited for React Server Components applications, where traditional error boundaries have limited utility.