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.
Overview
Section titled “Overview”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.
When to Use Each Handler
Section titled “When to Use Each Handler”onUncaughtError
Section titled “onUncaughtError”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
useEffector other lifecycle hooks. - Errors during React transitions.
"use client";
import { useEffect } from "react";
export function Component() { useEffect(() => { // This error will trigger onUncaughtError throw new Error("Lifecycle error"); }, []);
return <div>Component</div>;}onCaughtError
Section titled “onCaughtError”Use onCaughtError for errors that are caught by error boundaries:
- Component rendering errors
- Errors in component lifecycle methods
- Errors caught by
<ErrorBoundary>components
"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>;}Basic Setup
Section titled “Basic Setup”- Import
initClientfromrwsdk/client:
import { initClient } from "rwsdk/client";- Configure error handlers via
hydrateRootOptions:
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); }, },});- The error handlers will now catch and log all React errors in your application.
Universal Error Handling
Section titled “Universal Error Handling”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:
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 rejectionswindow.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(); }, },});Integration with Monitoring Services
Section titled “Integration with Monitoring Services”Sentry
Section titled “Sentry”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" }, }); }, },});Custom Monitoring Service
Section titled “Custom Monitoring Service”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"); }, },});Error Recovery Strategies
Section titled “Error Recovery Strategies”Show User-Friendly Messages
Section titled “Show User-Friendly Messages”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); }, },});Reload on Critical Errors
Section titled “Reload on Critical Errors”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."); } }, },});Best Practices
Section titled “Best Practices”1. Always Log Errors
Section titled “1. Always Log Errors”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};2. Include Component Stack
Section titled “2. Include Component Stack”The errorInfo.componentStack provides valuable debugging information. Always include it in your error reports:
Sentry.captureException(error, { contexts: { react: { componentStack: errorInfo.componentStack, }, },});3. Distinguish Error Types
Section titled “3. Distinguish Error Types”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" });},4. Don’t Block the UI
Section titled “4. Don’t Block the UI”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); }},Relationship to Error Boundaries
Section titled “Relationship to Error Boundaries”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).
Scope and Limitations
Section titled “Scope and Limitations”What These APIs Handle
Section titled “What These APIs Handle”- Component rendering errors (post-hydration).
- Errors inside
useEffector other lifecycle methods. - Errors during React transitions.
- Errors that escape error boundaries.
What They Don’t Handle Reliably
Section titled “What They Don’t Handle Reliably”- 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’sfetchmethod. - SSR errors: Handled server-side.
Common Patterns
Section titled “Common Patterns”Pattern 1: Development vs Production
Section titled “Pattern 1: Development vs Production”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); } }, },});Pattern 2: User Feedback
Section titled “Pattern 2: User Feedback”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}`); }, },});Summary
Section titled “Summary”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.