When building web applications, especially in frameworks like React and Next.js, you’ll inevitably encounter errors. These errors, if unhandled, can cause your application to crash or behave unpredictably. This is where Error Boundaries come to the rescue, ensuring that even if one part of your UI crashes, the rest of your application remains functional.
In this post, we will explore what error boundaries are, how they work, and how to implement them effectively in Next.js. We'll dive deep with examples, focusing on both client-side and server-side rendering scenarios.
What Are Error Boundaries?
Error boundaries are React components designed to catch JavaScript errors in their child component tree during rendering, in lifecycle methods, and within constructors. They do not catch errors in event handlers or asynchronous code like promises (but you can handle these differently, which we'll cover).
Why Do We Need Error Boundaries in Next.js?
Next.js leverages React under the hood, and as your application grows, it's essential to have mechanisms in place to gracefully handle runtime errors. Since Next.js has both server-side and client-side rendering, error boundaries can ensure that errors on the client side don’t crash the entire app.
For instance, you may have a component that fetches user data from an API, but due to network issues, the request fails. Without an error boundary, this could crash your entire React tree. However, with error boundaries, you can isolate the failure to just that component while providing a fallback UI like an error message.
How Error Boundaries Work
Error boundaries rely on two lifecycle methods:
static getDerivedStateFromError(error): This method is called when a child component throws an error. It allows you to update the component state to render a fallback UI.
componentDidCatch(error, info): This method logs the error information for debugging.
Next.js doesn’t provide built-in error boundary components, so you must implement them yourself. Let’s dive into the implementation.
Implementing Error Boundaries in Next.js
Here’s a simple implementation of an error boundary in Next.js.
Step 1: Create an Error Boundary Component
// components/ErrorBoundary.js
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state to render fallback UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Log the error to an external service
console.error("Error occurred:", error, errorInfo);
}
render() {
if (this.state.hasError) {
// Render fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;
This component wraps around your child components. When an error is thrown within its child components, it will catch that error and display a fallback UI (Something went wrong. in this case).
Step 2: Wrap the Error Boundary Around Components
Now, let’s use this error boundary in one of our pages in Next.js.
// pages/index.js
import ErrorBoundary from '../components/ErrorBoundary';
import FaultyComponent from '../components/FaultyComponent';
export default function Home() {
return (
<div>
<h1>Welcome to Next.js Error Boundaries Demo</h1>
<ErrorBoundary>
<FaultyComponent />
</ErrorBoundary>
</div>
);
}
Step 3: Simulate an Error in a Component
Next, let’s simulate an error in one of our components.
// components/FaultyComponent.js
import React from 'react';
const FaultyComponent = () => {
throw new Error("This is a simulated error!");
return <div>You'll never see this text.</div>;
}
export default FaultyComponent;
When you visit the homepage, instead of crashing the entire app, the error boundary will catch the error and display the fallback UI: "Something went wrong."
Enhancing the Fallback UI
Error boundaries don’t have to be boring! You can make the fallback UI more informative or user-friendly, such as adding retry buttons, error logging services, or even suggestions to contact support.
Here’s how you can improve the fallback UI:
// components/ErrorBoundary.js
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error("Error:", error, errorInfo);
// You can also log the error to an external service
}
handleRetry = () => {
this.setState({ hasError: false });
};
render() {
if (this.state.hasError) {
return (
<div>
<h2>Oops, something went wrong.</h2>
<p>Please try refreshing the page or contact support.</p>
<button onClick={this.handleRetry}>Retry</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Handling Asynchronous Errors
Error boundaries don’t catch errors in asynchronous code by default. If you’re working with async functions or promises, you need to handle errors within those manually.
Here’s how you can handle async errors in your components:
// components/AsyncComponent.js
import React, { useState, useEffect } from 'react';
const AsyncComponent = () => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch('/api/data');
if (!res.ok) {
throw new Error("Network response was not ok");
}
const json = await res.json();
setData(json);
} catch (err) {
setError(err);
}
};
fetchData();
}, []);
if (error) {
return <div>Error: {error.message}</div>;
}
return data ? <div>{data.message}</div> : <div>Loading...</div>;
};
export default AsyncComponent;
In this example, if an error occurs during data fetching, we display a user-friendly error message instead of crashing the entire app.
Server-Side Error Handling in Next.js
Next.js provides built-in error pages for server-side errors. For instance, you can customize the pages/_error.js file to handle server-side errors more gracefully. Here's a simple example:
// pages/_error.js
function Error({ statusCode }) {
return (
<p>
{statusCode
? `An error ${statusCode} occurred on server`
: 'An error occurred on client'}
</p>
);
}
Error.getInitialProps = ({ res, err }) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
return { statusCode };
};
export default Error;
With this setup, Next.js will display a custom error page for server-side errors, while you can use error boundaries for client-side rendering errors.
Conclusion
Error boundaries are crucial in modern React and Next.js applications, allowing you to catch and handle errors gracefully without crashing the entire app. In this post, we covered:
- The basic concept of error boundaries.
- How to implement a basic error boundary in Next.js.
- Handling asynchronous errors.
- Server-side error handling in Next.js. By utilizing error boundaries, you can improve the resilience and user experience of your application, making it more reliable in production environments.