1. đ Introduction
No application is bug-free, and network requests fail more often than we'd like. Error handling in React is about designing for that reality â catching failures gracefully, showing users something helpful instead of a blank screen, and giving developers enough information to fix problems quickly. This tutorial covers everything from Error Boundaries to API failures, form validation, logging, and production debugging.
Information
2. đ§Š Understanding Errors in React
Errors in a React app generally fall into two buckets: errors that happen during rendering (which React can catch with special tools) and errors that happen outside rendering â in event handlers, timers, or async code â which React cannot catch automatically.
3. đī¸ Types of Errors
- Rendering errors â thrown while a component function executes
- Runtime/JavaScript errors â logic bugs like calling a method on undefined
- Asynchronous errors â failed API calls, rejected promises
- Validation errors â user input that doesn't meet expected rules
4. âī¸ Runtime Errors
A runtime error occurs when otherwise valid JS code fails during execution â for example, accessing a property on null, or calling a function that doesn't exist.
RuntimeError.jsx
function UserGreeting({ user }) {
// Throws if user is undefined: "Cannot read properties of undefined"
return <h1>Hello, {user.name}</h1>;
}5. đŧī¸ Rendering Errors
A rendering error is any error thrown while a component's function body is executing â during the initial render or a re-render. These are the errors that Error Boundaries (Section 8) are specifically designed to catch.
6. đ JavaScript Errors
Standard JavaScript error types â TypeError, RangeError, ReferenceError â behave the same way inside React components as anywhere else in JS, but their consequences differ depending on where they occur.
7. âŗ Asynchronous Errors
Errors thrown inside a Promise, setTimeout, or an event handler happen outside React's render cycle, so Error Boundaries cannot catch them â they must be handled manually with try/catch or .catch().
AsyncError.jsx
async function loadUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error("Failed to load user");
return await response.json();
} catch (error) {
console.error(error);
throw error; // re-throw or handle locally
}
}Important
8. đĄī¸ Error Boundaries
An Error Boundary is a special component that catches JavaScript errors thrown anywhere in its child component tree during rendering, logging them and displaying a fallback UI instead of crashing the whole app.
9. đī¸ Creating Error Boundaries
Error Boundaries must currently be written as class components, using the static getDerivedStateFromError and componentDidCatch lifecycle methods.
ErrorBoundary.jsx
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error("Caught by ErrorBoundary:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}Tip
10. đ§Š Using Error Boundaries
Wrap Error Boundaries around logical sections of your app â a widget, a route, a sidebar â so a failure in one part doesn't take down the entire page.
UsingErrorBoundary.jsx
function App() {
return (
<div>
<Header />
<ErrorBoundary fallback={<p>Something went wrong loading the dashboard.</p>}>
<Dashboard />
</ErrorBoundary>
<Footer />
</div>
);
}Best Practice
11. đ¨ Error Fallback UI
A good fallback UI explains what happened in plain language and, where possible, offers a way to recover â like a "Try again" button.
ErrorFallback.jsx
function ErrorFallback({ onRetry }) {
return (
<div role="alert">
<p>We couldn't load this section.</p>
<button onClick={onRetry}>Try again</button>
</div>
);
}12. âģī¸ Error Recovery
A boundary can expose a way to reset its own error state, letting the user retry without reloading the entire page.
RecoverableBoundary.jsx
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
handleRetry = () => this.setState({ hasError: false });
render() {
if (this.state.hasError) {
return <ErrorFallback onRetry={this.handleRetry} />;
}
return this.props.children;
}
}13. đ Global Error Handling
Beyond component-level boundaries, window.onerror and window.addEventListener("unhandledrejection") catch truly uncaught errors and rejected promises anywhere in the app â a useful last line of defense for logging.
GlobalErrorHandler.jsx
window.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled promise rejection:", event.reason);
logErrorToService(event.reason);
});14. đ Handling API Errors
Always check the response status and distinguish between different error types â a 404 deserves different messaging than a 500 or a network failure.
ApiErrorHandling.jsx
async function fetchProduct(id) {
const response = await fetch(`/api/products/${id}`);
if (response.status === 404) {
throw new Error("Product not found");
}
if (!response.ok) {
throw new Error("Something went wrong loading this product");
}
return response.json();
}15. đĄ Handling Network Errors
A network error â like a lost connection â causes fetch itself to reject, rather than returning a response object at all. This must be caught separately from HTTP error statuses.
NetworkErrorHandling.jsx
try {
const response = await fetch("/api/products");
// ...
} catch (error) {
if (error instanceof TypeError) {
console.error("Network error - check your connection");
}
}16. đ Handling Form Errors
Form errors should be stored in component state and displayed next to the relevant field, rather than as a single generic message at the top of the form.
FormErrorHandling.jsx
function SignupForm() {
const [errors, setErrors] = useState({});
const handleSubmit = async (e) => {
e.preventDefault();
try {
await submitForm(formData);
} catch (error) {
setErrors({ form: "Signup failed. Please try again." });
}
};
return (
<form onSubmit={handleSubmit}>
{errors.form && <p role="alert">{errors.form}</p>}
{/* fields */}
</form>
);
}17. â Handling Validation Errors
Validation errors are typically produced before a request is even sent, by checking values against expected rules (required fields, formats, ranges).
ValidationErrors.jsx
function validate(values) {
const errors = {};
if (!values.email.includes("@")) errors.email = "Enter a valid email address";
if (values.password.length < 8) errors.password = "Password must be at least 8 characters";
return errors;
}18. đ Handling Authentication Errors
A 401 Unauthorized response usually means the session has expired â the standard response is to redirect the user to a login page, ideally preserving where they were headed.
AuthErrorHandling.jsx
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
navigate("/login", { state: { from: location } });
}
return Promise.reject(error);
}
);19. âŗ Handling Suspense Errors
Components using Suspense for data fetching (via libraries built for it, like TanStack Query's suspense mode) throw errors that must be caught by a nearby Error Boundary, not by Suspense itself.
SuspenseWithErrorBoundary.jsx
<ErrorBoundary fallback={<p>Failed to load data.</p>}>
<Suspense fallback={<Spinner />}>
<ProductDetails />
</Suspense>
</ErrorBoundary>20. đĒĩ Logging Errors
Every caught error should be logged somewhere useful â at minimum the console during development, and a proper logging service in production.
ErrorLogging.jsx
componentDidCatch(error, errorInfo) {
console.error(error, errorInfo);
logErrorToService({ error: error.message, stack: error.stack, componentStack: errorInfo.componentStack });
}21. đĄ Error Monitoring
Production apps typically integrate a dedicated monitoring service (like Sentry or LogRocket) that automatically captures errors, stack traces, and often session replays â giving visibility that console.error alone can't provide.
Reference
22. đ Debugging React Applications
React DevTools shows the component tree, current props/state, and highlights re-renders â an essential companion to the browser's built-in JS debugger for tracking down the source of an error, not just its symptom.
23. đ Stack Traces
A stack trace shows the sequence of function calls that led to an error, including â in development â the specific component in which it occurred, thanks to React's component stack.
24. đēī¸ Source Maps
Production builds are typically minified, making raw stack traces unreadable. Source maps translate minified code locations back to the original source files and line numbers, which monitoring tools use to show readable traces.
25. đ Development vs Production Errors
React shows much more detailed error overlays and warnings in development mode. In production, these are stripped for performance and security, so errors should be sent to a monitoring service rather than relying on what users see.
| Aspect | Development | Production |
|---|---|---|
| Error overlay | Full stack trace, component stack | None (silent unless boundary catches it) |
| Warnings | Verbose console warnings | Stripped out |
| Debugging tool | Browser console + React DevTools | Monitoring service + source maps |
26. đī¸ Graceful Degradation
When part of a page fails, the rest should ideally keep working â a failed recommendations widget shouldn't take down an entire product page. This is the core motivation behind scoping Error Boundaries narrowly (Section 10).
27. đˇ TypeScript with Error Handling
Since caught errors are typed as unknown in TS, always narrow them before accessing properties like .message.
TypedErrorHandling.tsx
try {
await submitForm(data);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "An unknown error occurred";
setErrors({ form: message });
}28. đ Best Practices
- Scope Error Boundaries around independent sections rather than one giant boundary at the root
- Always distinguish network errors, HTTP error statuses, and validation errors â they need different handling
- Log every caught error to a monitoring service, not just the console
- Give users a way to recover â retry buttons, links home â instead of a dead end
- Never silently swallow an error with an empty catch block
29. â ī¸ Common Mistakes
- Assuming Error Boundaries catch all errors, including those in event handlers or async code
- Wrapping the entire app in a single Error Boundary, so any failure blanks the whole UI
- Showing raw error messages or stack traces directly to end users
- Forgetting to check response.ok, treating a failed HTTP request as a success
- Catching an error and doing nothing with it, hiding real bugs from both users and developers
Danger
30. đŦ Frequently Asked Questions
Do Error Boundaries catch errors in event handlers?
No â Error Boundaries only catch errors thrown during rendering. Errors in event handlers, setTimeout, or async callbacks must be handled with a regular try/catch.
Can I write an Error Boundary as a function component?
Not directly â getDerivedStateFromError and componentDidCatch currently require a class component. Libraries like react-error-boundary wrap this in a hook-friendly API if you'd rather avoid writing the class yourself.
Should I show the actual error message to users?
Generally no â raw error messages can expose internal details or simply confuse users. Show a friendly, actionable message instead, and log the technical details to your monitoring service.