We cover how to handle errors on both the client and server sides, explaining error handling and its process and the types of errors specific to Next.js apps.
Handling errors effectively is an important aspect of web application development that cannot be ignored. To provide an easy user experience and optimal performance, developers must learn how to handle errors effectively and efficiently in their applications.
Error handling defines how software applications respond and recover from unexpected situations. It is the process of anticipating, detecting and recovering from errors—also known as exceptions that may occur during the execution of a program. It serves as a stronger layer of protection, so that when unexpected issues arise, they can be handled gracefully, maintaining the integrity and usability of the application.
In this article, we will look at the intricacies of error handling in modern Next.js applications. Next.js is a React framework for building full-stack web applications. It offers two ways of handling errors: client-side and server-side error handling. Understanding this will help us build applications with a better user experience.
The process of error handling consists of three main stages: detection, handling and resolution. Each of these stages plays an important role in maintaining the stability of an application and improving its user experience.
Next.js primarily encounters two broad categories of errors: client-side and server-side errors.
Client-side errors occur in the user’s browser after the initial HTML and JavaScript have been downloaded from the server. These errors prevent the application from functioning as intended on the client side. Now, different things can cause this kind of error during code execution. The subsequent subsections highlight some of these causes:
Syntax errors occur when the code does not follow the syntactic rules of the programming language. In JavaScript, these errors can result from typographical errors, missing brackets or semicolons or other syntax-related mistakes. These errors prevent the browser from interpreting or executing the code.
Here is an example of a syntax error:
// Missing closing parenthesis
console.log("Hello, world!";
Because of the missing parenthesis in the code above, the code will not execute until it is corrected.
Runtime errors occur when the code, though syntactically correct, encounters an issue during execution. These errors are also referred to as exceptions. They can result from various issues, such as referencing undefined variables, dividing by zero or calling methods on null objects.
Here is an example of a runtime error:
// Attempting to access a property of undefined
let person = {};
console.log(person.name.toUpperCase());
Here, we are trying to access a property that has not been defined. When we execute this code, it will throw an error, as shown in the image below:
Data fetching errors occur when attempts to fetch data from external sources, such as APIs, fail due to network issues, invalid API endpoints or inappropriate response handling.
The code below shows an instance of a data fetching error:
// Using fetch to request data from an API
fetch("https://api.example.com/data")
.then((response) => {
if (!response.ok) {
throw new Error("Failed to fetch data");
}
return response.json();
})
.then((data) => console.log(data))
.catch((error) => console.error("Data fetching error:", error));
Server-side errors are issues that occur on the server side of an application, not within the user’s browser or client-side environment. These errors usually involve problems with data processing, database interactions or server-side rendering. In Next.js, server-side errors fall into two main categories: API route errors and layout resolution errors.
In Next.js, API routes are a specific type of route used to create server-side endpoints for your application. These endpoints handle data fetching, logic execution and communication between your frontend and backend. However, errors can occur within these API routes.
API route errors occur when there is an issue with processing requests to these server-side routes. Examples of this error include database connection errors, such as failure to connect to the database or database query errors, and errors during data retrieval or manipulation in server-side code.
API route errors can result in failed requests, incomplete or incorrect data retrieval, and disrupted functionality of the application’s server-side features.
Layout resolution errors occur when Next.js cannot resolve or identify the specified layout components for rendering pages. Layout components are used to define the structure and common elements of pages in Next.js applications.
Examples of these errors can be attempting to use a layout component that does not exist or has been moved, an incorrect configuration of layout components in page components, errors in resolving layout components due to file system issues or misconfiguration.
Layout resolution errors can cause pages to be rendered without the intended layout structure, leading to inconsistent or broken page layouts across the application.
In Next.js, client-side errors like syntax, runtime and data fetching errors can stop your application from running if not handled effectively. The best way to prevent and fix our application from crashing is through the use of an error boundary.
Typically, whenever an error occurs during rendering in your React application, React completely removes the UI, thereby making your entire application crash. An error boundary acts like a safety net for your application. It catches errors that occur anywhere within its child component tree, preventing the entire application from crashing. When an error boundary catches an error, it provides a fallback UI and logs the error information.
In previous versions of Next.js, it has helped developers with error handling through the use of custom error pages like 404 and 500 pages. However, these pages have limitations within the Pages Router. such as limited customization, limited error information and outdated support for React error boundaries.
With the release of the new App Router, handling errors has greatly improved the developer experience.
To set up error boundaries in our project, we first need to create a Next.js project. You can do so by running the following command on your terminal:
npx create-next-app@latest my-nextjs-app
After setting up our project, we need a nested route page to implement error boundaries in our Next.js application.
First, we need to create the pages for the routes. Create a page.js
, file in your app/adminDashboard
folder and add the following to it:
export default function Admin() {
return (
<div className="items-center flex flex-col justify-center mb-4">
This is the Admin page!
</div>
);
}
Then, for your nested route, create another page.js
file in your app/adminDashboard/profile
folder and add the following code to it:
export default function Profile() {
return (
<div className=" items-center flex justify-center mb-4">
This is Profile Page!
</div>
);
}
Now, to be able to handle errors in our application, we will intentionally simulate a network error in the profile page route. Update the app/adminDashboard/profile.js
file with the following code:
"use client";
import { useEffect, useState } from "react";
function simulateNetworkError(delay = 1000) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("Network Error"));
}, delay);
});
}
export default function Profile() {
const [error, setError] = useState(null);
const fetchData = async () => {
try {
await simulateNetworkError();
} catch (error) {
setError(error);
}
};
useEffect(() => {
fetchData();
}, []);
return (
<div className="items-center flex justify-center mb-4">
{error ? <div> {error}</div> : <div>This is Profile Page!</div>}
</div>
);
}
In the code above, we created a simulateNetworkError
function that creates a promise to simulate a network error after a second delay. This will create a runtime error for our application, as seen in the image below.
To handle this error with an error boundary, create an error.js
file in your app
directory, app/error.js
, and add the following code to it:
"use client";
import { useEffect } from "react";
export default function Error({ error, reset }) {
useEffect(() => {
// Log the error
console.error(error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center mt-52">
<h2 className="mb-8 text-xl-2">Something went wrong!</h2>
<div className="mt-4">
<button
className="p-4 bg-red-500 rounded-md"
// Attempt to recover by trying to re-render the segment
onClick={() => reset()}
>
Try again
</button>
</div>
</div>
);
}
In the code above, the error.js
file automatically creates a React error boundary that wraps a nested child segment or page.js
component. If any error occurs within the route segment, the React component exported from the error.js
file is used as the fallback UI instead of crashing the entire application.
Some errors can be temporary and simply trying again might resolve the issue. This is where the reset
function comes in. It prompts the user to recover from the error by attempting to re-render the error boundary’s contents. If successful, the fallback error component is replaced with the result of the re-render, as shown in the image below.
In Next.js, if an error occurs in a child component of the error boundary, it will be caught. However, error boundaries defined in error.js do not catch errors thrown in layout.js components of the same segment.
Let’s see this in our demo application. Create a components
folder in the app
folder. Inside the components
folder, create a Navbar.js
file and add the following code to it:
"use client";
import { useRouter } from "next/navigation";
export default function Navbar() {
const router = useRouter();
return (
<nav className="bg-gray-800 h-full w-64 fixed top-0 left-0 flex flex-col justify-between">
<ul className="flex flex-col mt-6">
<li
className="text-white py-2 px-4 cursor-pointer hover:bg-gray-700"
onClick={() => router.push("/")}
>
Home
</li>
<li
className="text-white py-2 px-4 cursor-pointer hover:bg-gray-700"
onClick={() => router.push("/dashboard")}
>
Admin
</li>
<li
className="text-white py-2 px-4 cursor-pointer hover:bg-gray-700"
onClick={() => router.push("/dashboard/profile")}
>
Profile
</li>
</ul>
</nav>
);
}
In the code above, we used the useRouter
hook from next/navigation
to easily navigate between our routes.
Create a layout.js
file in your app/dashboard
folder and add the following code to it:
"use client ";
import Navbar from "../components/Navbar";
export default function AdminDashboardLayout({ children }) {
return (
<>
<Navbar />
{children}
</>
);
}
In the code above, we rendered the Navbar
component in the dashboard page and passed the children
prop. Now, when you navigate to the dashboard page, it works perfectly, but when you click on the profile page, it shows the fallback UI and crashes the entire application. That is because we are handling the error in the same segment.
To resolve this, create an error.js
file in your app/dashboard/profile
folder and add the following code to it:
"use client";
import { useEffect } from "react";
export default function Error({ error, reset }) {
useEffect(() => {
// Log the error
console.error(error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center mt-52">
<h2 className="mb-8 text-xl-2">Something went wrong!</h2>
<div className="mt-4">
<button
className="p-4 bg-red-500 rounded-md"
// Attempt to recover by trying to re-render the segment
onClick={() => reset()}
>
Try again
</button>
</div>
</div>
);
}
With the error.js
file in the nested route, we can catch the error early and still maintain interactivity in other parts of our application, as seen in the image below.
To handle errors in your application’s root layout, create a global-error.js
file. The global-error.js
file wraps the entire application and the fallback component replaces the root layout when active.
When an error occurs in a server component, Next.js will forward an Error object to the nearest error.js
file. For security reasons, Next.js sends only generic messages, to avoid leaking potentially sensitive information to the client.
We can handle the API route errors by using try/catch
blocks to catch errors and return appropriate error responses to the client.
// pages/api/data.js
import fetch from "node-fetch";
export default async function handler(req, res) {
try {
const response = await fetch("https://api.example.com/data");
if (!response.ok) {
throw new Error("Failed to fetch data");
}
const data = await response.json();
res.status(200).json(data);
} catch (error) {
console.error("Error fetching data:", error);
res.status(500).json({ message: "Internal Server Error" });
}
}
In the example above, we used fetch
to make a GET
request to an external API endpoint. If the response is unsuccessful (!response.ok
), we throw an error, and if it is successful, we return the data. If any error occurs during the fetch operation, we catch it, log it, and return a 500 internal server error response.
Developers must manage errors effectively within their applications to provide a better user experience. In this article, we covered how to handle errors on both the client and server side, explaining error handling and its process and detailing the types of errors specific to Next.js applications.
Chris Nwamba is a Senior Developer Advocate at AWS focusing on AWS Amplify. He is also a teacher with years of experience building products and communities.