Error Handling
How to handle React errors in your RedwoodSDK application using React 19's error handling APIs
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
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
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.
:::caution
Errors in imperative event handlers (e.g., onClick) or asynchronous timers (e.g., setTimeout) often bubble directly to the browser and may not be caught by onUncaughtError. For these, you should use global browser handlers (see Universal Error Handling below).
:::
"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
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
- 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
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 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();
},
},
});Integration with Monitoring Services
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
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
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
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
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
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
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
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);
}
},Server-Side Error Handling
For server-side errors (errors in Server Components, middleware, route handlers, and RSC actions), use the except function from rwsdk/router. This provides a declarative way to handle errors that integrates with your routing structure.
Basic Usage
import { except, route } from "rwsdk/router";
import { defineApp } from "rwsdk/worker";
export default defineApp([
except((error) => {
console.error("Server error:", error);
return <ErrorPage error={error} />;
}),
route("/", () => <HomePage />),
]);Integration with Monitoring
You can combine except with monitoring services. Since monitoring calls are often asynchronous, use ctx.waitUntil() to ensure the worker doesn't terminate before the error is sent:
import { except, route } from "rwsdk/router";
import { defineApp } from "rwsdk/worker";
export default defineApp([
except(async (error, { request, cf: ctx }) => {
// Send to monitoring service asynchronously without blocking the response
ctx.waitUntil(
sendToMonitoring(error, {
url: request.url,
method: request.method,
}),
);
// Return user-friendly error page
return <ErrorPage error={error} />;
}),
route("/", () => <HomePage />),
]);Nested Error Handling
You can define multiple except handlers for different sections of your application:
import { except, prefix, route } from "rwsdk/router";
import { defineApp } from "rwsdk/worker";
export default defineApp([
// Global error handler
except((error) => {
return <GlobalErrorPage error={error} />;
}),
prefix("/api", [
// API-specific error handler
except((error) => {
return Response.json(
{ error: error instanceof Error ? error.message : "API Error" },
{ status: 500 },
);
}),
route("/users", async () => {
// This error will be caught by the API handler
throw new Error("Database error");
}),
]),
route("/", () => <HomePage />),
]);For more details on except, see the router documentation.
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 except function from rwsdk/router (see router error handling documentation).
Scope and Limitations
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
- 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 the
exceptfunction or wrapdefineApp'sfetchmethod. - SSR errors: Handled server-side.
For server-side error handling, see the router documentation on error handling.
Common Patterns
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
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
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.