Create a custom error component in Next.js (SSR & CSR)

Next.js comes with great support for error handling. In the following article, I am going to show you how to implement a personalized error component by building a tiny application that fetches data from the Rick and Morty API and can handle errors both on the server-side and the client-side.

If you want to go straight to the code, here is the repo: Next.js Error Component
Let's dive right in:

1. Set up a tiny project and simulate some errors!

Feel free to skip this part if you already have an application up and running that throws some errors 😉

  • First, create a fresh Next.js project by running npx create-next-app@latest custom-error-component
  • Verify that everything worked out by running npm run dev inside that newly created directory and inspecting the Next.js default page on localhost:3000
  • We now will create the three pages our app consists of. First, replace the code in index.js with the following:
import Link from 'next/link';

export default function Home() {
  return (
    <div className="home-container">
      <h2>Welcome to my amazing Rick and Morty Page!</h2>
      <div className="img-container">
        <img src="https://rickandmortyapi.com/api/character/avatar/2.jpeg"></img>
        <img src="https://rickandmortyapi.com/api/character/avatar/1.jpeg"></img>
      </div>
      <div className="link-container">
        <Link href="/characters">
          <a>
            Show me Rick and Morty Characters!
          </a>
        </Link>
        <Link href="/locations">
          <a>
            Show me Rick and Morty locations!
          </a>
        </Link>
      </div>
    </div>
  )
}

For the CSS, just grab this and copy it into the globals.css file:

html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}

a {
  color: inherit;
  text-decoration: none;
}

* {
  box-sizing: border-box;
}

/* styles for index.js */

.home-container {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  gap: 2rem;
  justify-content: center;
}

.home-container h2 {
  text-align: center;
}

.img-container {
  display: flex;
  gap: 2rem;
  justify-content: center;
}
a {
  border: 1px solid black;
  padding: 0.6rem 1rem;
  border-radius: 5px;
}

.link-container {
  display: flex;
  justify-content: center;
  gap: 2rem;
}

/* styles for locations.js */

.locations-container {
  max-width: 1100px;
  margin: auto;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2rem;
  margin-block: 2rem;
}

.locations-container a {
  align-self: baseline;
  justify-self: baseline;
}

.locations-container a:nth-of-type(2) {
  justify-self: end;
}

.locations-container h2 {
  text-align: center;
}

article {
  border: 1px solid black;
  border-radius: 5px;
  padding: 0 1rem;
}

/* styles for characters.js */

.character-card {
  padding: 1rem;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 1rem;
}

/* styles for the error page */

.error-container {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

As you can see, this shows 2 pictures and renders Links to the two remaining pages. Very simple.
A picture of index.js

  • Now, create a file called locations.js - we want to fetch data on the client-side and display a list of Rick and Morty locations on this page. Our code looks like this:
import React, { useEffect, useState } from 'react';
import Link from 'next/link'

function Locations(rops) {
const [locations, setLocations] = useState({});
useEffect( () => {
    fetch("https://rickandmortyapi.com/api/location")
    .then(res => res.json())
    .then(data => setLocations(data));
}, [])

    return (
        <div className="locations-container">
        <Link href="/"><a>Back to home</a></Link>
        <h2>Rick and Morty Locations</h2>
        <Link href="/characters"><a>Show me Rick and Morty Characters!</a></Link>
        {

            locations.results?.map((location) => (
                <article key={location.id}>
                    <p>Name: {location.name}</p>
                    <p>Dimension: {location.dimension}</p>
                    <p>Type: {location.type}</p>

                </article>))
        }            
        </div>
    );
}

export default Locations;

We are just fetching the data inside the useEffect hook and reading it into state. Then, we map over the data and display some decent looking cards on our page:
Capture of Locations page

Please go ahead and improve this visually if your design skills are better than mine, but I didn't want to write too much CSS and distract from the actual topic.

Note the ? in locations.results?.map - This is called optional chaining. If the data takes a while to be fetched, React will try to map over locations.results but there will be no locations.results yet and our application will break. With conditional chaining, React will not try to iterate if there is no data yet, and just display the title and the buttons.

  • For the characters page, we are going to implement server-side rendering with getServerSideProps:
import React from 'react';
import Link from 'next/link'

function Characters(props) {
    return (
        <div className="locations-container">
        <Link href="/"><a>Back to home</a></Link>
        <h2>Rick and Morty Characters</h2>
        <Link href="/locations"><a>Show me Rick and Morty Locations!</a></Link>
            {
                props.characters.results.map( (character) => (
                    <article key={character.id} className="character-card">
                        <img src={character.image} alt={character.name} height="200px" />
                        <div>
                            <p>{character.name}</p>
                            <p>{character.location.name}</p>
                        </div>
                    </article>
                ))
            }
        </div>
    );
}

export default Characters;

export async function getServerSideProps(context) {
    const characters  = await fetch("https://rickandmortyapi.com/api/character").then(res => res.json());
    return { props: { characters}}
}

The function getServerSideProps will get called before the component is mounted. Then, it will pass the data via props to the component and render it out. We won't be able to see the request in the network tab of our development tools, because the development server is doing the fetching before sending the page to our browser. We also don't need any conditional chaining here since there won't be a moment where the component is waiting for data.

Our characters page will look something like this:
Capture of characters page

Beautiful! We can now navigate around and everything works just fine. But what happens if the Rick and Morty API changes or breaks? Let's try:

2. Producing some errors

You might think, we have a little problem here: The Rick and Morty API is not under our control, so we can't break it or force it to return errors to us. That's true and I chose the API on purpose because of that. We will have to get creative to simulate some errors:

  • go locations.js and change location in the API call for something else, like for example locaon
  • in characters.js, do the same. Replace character with charter or something else that does not exist.
  • stop your dev build with hot reloading and let's take a look at how these errors would look like in production.
  • run npm run build to create a production build
  • run npm run start to start that production build locally.
  • open the build on localhost:3000
  • navigate around and see what the pages look like: Error behavior in locations In locations, we will just see the title and the buttons, since our conditional chaining is protecting us from errors. However, this is quite confusing for the user. There is no data and also no feedback on why there is no data.

Let's fix this:

3. Create our custom error component

First of all, stop the production build and return to your hot-reloaded dev build.
To create a custom error component, we have to create a file called _error.js in the pages folder. You can find the documentation about this page in the Next.js docs: Customized Error Component. Copy and paste the code from the docs and adapt it to go with the look and feel of your application. Mine looks like this:

import Link from 'next/link';

function Error({ statusCode }) {
    return (
      <div className="error-container">
      <img src="https://rickandmortyapi.com/api/character/avatar/234.jpeg" alt="a dead morty..."/>
        {statusCode && <h1>Error: {statusCode}</h1>}
        <p>We are sorry! There was an error</p>
        <Link href="/">
            <a>Go back home</a>
        </Link>
      </div>
    )
  }

  Error.getInitialProps = ({ res, err }) => {
    const statusCode = res ? res.statusCode : err ? err.statusCode : 404
    return { statusCode }
  }

Now how do we show it?
Let's look at locations.js first. Leave the typo we introduced earlier and call https://rickandmortyapi.com/api/locaon instead of location, we will get back an object looking like this: { error: 'There is nothing here'}.
With this, I can conditionally render the Error component I just created:

const [locations, setLocations] = useState({});
useEffect( () => {
    fetch("https://rickandmortyapi.com/api/locaon")
    .then(res => res.json())
    .then(data => setLocations(data));
}, [])

if(locations.error) {
    return <Error />
}
 return (
        <div className="locations-container">
        <Link href="/"><a>Back to home</a></Link>

As a result, you will see the following on your locations-page:
Caption of error page
You might have noticed that when you reload the page, there is a slight flicker, where you first see the title of the page and then the error. The following is happening:

  1. Next.js renders the buttons and the title. Since there is not yet a locations.results, it doesn't render those.
  2. Concurrently, it is trying to fetch the data inside the useEffect hook.
  3. Once the data is fetched, the state is updated, which triggers a rerender.
  4. Since there is an error, the Error component gets rendered instead of the title and buttons.

Now fix the URL and see how the original page reappears. 🙂

How can we tackle the server-side rendered page? We need to take into account that the Next.js documentation explains that this error component is only shown in production, and we will see a stack trace in our dev environment. To test our error component is working for SSR, leave the typo in the API call and create a new production build as we did before.

  • stop npm in your terminal and run npm run build
  • once that is finished, run npm run start. Now you can visit your production build on localhost:3000
  • when you navigate to localhost:3000/characters, you will see our error component, with additional information we didn't see in our client-side rendered component:

What is happening?

  • When we make the petition to the server, it tries to fetch the data, but encounters an error.
  • Therefore, it directly returns the error page instead of the characters page and you won't see the flicker you saw with client-side rendering.
  • Also note that we didn't modify the code in characters.js itself, besides obviously introducing the error.

That's it! You now have personalized error components that show when your app encounters errors on both server-side and client-side.

Sidenote

Most APIs return status codes and descriptive error messages you can display inside your Error component if you want to. Play around with that - this code is just a template to get you started.

I hope that this was helpful! Have a great week everybody. 🤓

23