---
title: "Automating error pages based on API responses"
description: "How we automated displaying error pages based on API responses"
canonical_url: "https://www.bigbinary.com/blog/automatically-displaying-errors"
markdown_url: "https://www.bigbinary.com/blog/automatically-displaying-errors.md"
---

# Automating error pages based on API responses

How we automated displaying error pages based on API responses

- Author: Farhan CK
- Published: May 28, 2024
- Categories: ReactJS, Rails

Error handling is an important aspect of building software. We've automated
error handling wherever possible so that we can deal with it in a consistent
manner. By automating this process, we've allowed our product engineers to focus
on shipping quality software.

This blog is about how we automated handling and displaying error pages in
[neeto](https://www.neeto.com/) applications.

Before we jump in, let's look at [Axios](https://axios-http.com/) and
[Zustand](https://github.com/pmndrs/zustand), two npm packages we heavily rely
on.

#### Axios

We use [Axios](https://axios-http.com/) to make API calls. Axios allows us to
intercept API calls and modify them if necessary.

```js
axios.interceptors.request.use(request => {
  // intercept requests
});
axios.interceptors.response.use(request => {
  // intercept responses
});
```

This feature is useful for tasks such as setting authentication tokens, handling
case conversion for requests and responses, cleaning up sensitive headers when
sending requests to third-party domains, etc. This is also a good place to
universally handle any errors on API calls.

#### Zustand

[Zustand](https://github.com/pmndrs/zustand) is a small, fast, scalable,
barebones state management solution using simplified flux principles. We use
Zustand for state management instead of Redux/React context. Here is a simple
example of how to create a store using Zustand.

```js
import { create } from "zustand";

const useBearStore = create(set => ({
  bears: 0,
  increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}));
```

To learn more about Zustand, check out there
[documentation](https://zustand-demo.pmnd.rs/).

### Detecting errors

Let's look into how we handle errors in our applications. As a first step, let's
examine the universal Axios response interceptor we wrote to detect any error.

```js
import axios from "axios";

axios.interceptors.response.use(null, error => {
  if (error.response?.status === 401) {
    resetAuthTokens();
    window.location.href = `/login?redirect_uri=${encodeURIComponent(
      window.location.href
    )}`;
  } else {
    const fullUrl = error.request?.responseURL || error.config.url;
    const status = error.response?.status;
    //TODO: Notify the user that an error is occurred.
  }
  return Promise.reject(error);
});
```

For each response, if it detects a 401 HTTP error, the code resets the
authentication tokens and redirects the user to the login page. For any other
error, we need to notify the user with an appropriate message.

### Storing errors

When an error happens, the user could be on any page, so we should be able to
display an error message no matter which page the user is on. For that, we need
to store the error in a way that it can be retrieved from anywhere. This is
where Zustand comes into play. It is very easy to create a store in Zustand and
can be used as a hook in React applications.

```js
import { prop } from "ramda";
import { create } from "zustand";

const useDisplayErrorPage = () => useErrorDisplayStore(prop("showErrorPage"));

export const useErrorDisplayStore = create(() => ({
  showErrorPage: false,
  statusCode: 404,
  failedApiUrl: "",
}));

export default useDisplayErrorPage;
```

The code snippet above creates a Zustand store for storing error-related data.
Additionally, it provides a hook that conveniently checks whether an error has
occurred anywhere within the application.

Back to our Axios interceptor, we can store the error using the
`useDisplayErrorPage` hook.

```js {2, 13-17}
import axios from "axios";
import { useErrorDisplayStore } from "./useDisplayErrorPage";

axios.interceptors.response.use(null, error => {
  if (error.response?.status === 401) {
    resetAuthTokens();
    window.location.href = `/login?redirect_uri=${encodeURIComponent(
      window.location.href
    )}`;
  } else {
    const fullUrl = error.request?.responseURL || error.config.url;
    const status = error.response?.status;
    useErrorDisplayStore.setState({
      showErrorPage: true,
      statusCode: status,
      failedApiUrl: fullUrl,
    });
  }
  return Promise.reject(error);
});
```

### Display errors

Now, we can use the `useDisplayErrorPage` hook at the root of our React
application. When an error happens, `showErrorPage` will become `true`; we can
use that to show an error page.

```jsx
import useDisplayErrorPage from "./useDisplayErrorPage";
import ErrorPage from "./ErrorPage";
const Main = () => {
  const showErrorPage = useDisplayErrorPage();
  if (showErrorPage) {
    return <ErrorPage />;
  }

  return <>Our App</>;
};
```

Let's look at the `ErrorPage` component. The component reads the error data from
the Zustand stores and displays the appropriate error message and the picture.

```jsx
import { useErrorDisplayStore } from "./useDisplayErrorPage";
import { shallow } from "zustand/shallow";

const ERRORS = {
  404: {
    imageSrc: "not-found.png",
    errorMsg: "The page you're looking for can't be found.",
    title: "Page not found",
  },
  403: {
    imageSrc: "unauthorized.png",
    errorMsg: "You don't have permission to access this page.",
    title: "Unauthorized",
  },
  500: {
    imageSrc: "server-error.png",
    errorMsg:
      "The server encountered an error and could not complete your request.",
    title: "Internal server error",
  },
};

const ErrorPage = ({ statusCode }) => {
  const { storeStatusCode, showErrorPage } = useErrorDisplayStore(
    pick(["statusCode", "showErrorPage"]),
    shallow
  );
  const status = statusCode || storeStatusCode;
  const { imageSrc, errorMsg, title } = ERRORS[status] || ERRORS[404];

  return (
    <div className="flex flex-col items-center justify-center h-screen">
      <title>{title}</title>
      <img src={imageSrc} className="mb-4" alt="Error Image" />
      <div className="text-lg font-medium">{errorMsg}</div>
    </div>
  );
};
```

## Custom error pages in Rails

Ruby on Rails comes with default error pages for commonly encountered requests
such as 404, 500, and 422 errors. Each request has an associated static HTML
page located within the public directory. Even though we can customize them to
look like our `ErrorPage` component, maintaining error pages in different places
will be difficult, and in the long run, they can go out of sync. So, we decided
to use the `ErrorPage` component for these scenarios as well.

To accomplish this, we'll start by creating a controller named
`ErrorsController`. This controller will contain a single show action, where
we'll extract the error code from the raised exception and render the
appropriate view.

```rb
class ErrorsController < ApplicationController
  before_action :set_default_format

  def show
    @status_code = params[:status_code] || "404"
    error = @status_code == "404" ?  "Not Found" : "Something went wrong!"

    if params[:url]
      Rails.logger.warn "ActionController::RoutingError (No route matches [#{request.method}] /#{params[:url]})"
    end

    respond_to do |format|
      format.json { render json: { error: }, status: @status_code }
      format.any { render status: @status_code }
    end
  end

  private

    def set_default_format
      request.format = :html unless request.format == :json
    end
end

```

Next, we'll create a `view` file for this show action. In this view file, we
will render the `Error` component and pass `error_status_code` to it as props.

```rb
<%= react_component("Error", { error_status_code: @status_code }, { class: "root-container" }) %>
```

In the `Error` component, we will render the same `ErrorPage` component created
above and pass the error status code received.

```jsx
import React from "react";
import ErrorPage from "./ErrorPage";

const reactProps = JSON.parse(
  document.getElementsByClassName("root-container")[0]?.dataset?.reactProps ||
    "{}"
);

const Error = () => <ErrorPage status={reactProps.error_status_code} />;

export default Error;
```

Now add the following route in the `config/routes.rb` file to point `404` and
`500` requests to the `show` action in the `ErrorsController`.

```rb
Rails.application.routes.draw do
  match "/:status_code", constraints: { status_code: /404|500/, format: :html }, to: "errors#show", via: :all
end
```

Finally, we need to tell the Rails application to use our new controller and
routes setup instead of those HTML templates in the public directory. For this,
add the following to the `class Application < Rails::Application` block in
`config/application.rb` file.

```rb
config.exceptions_app = self.routes
```

### Handling unmatched routes

Sometimes, we get random requests from bots and crawlers to access the paths
that don't exist. When this occurs, our server raises an
`ActionController::RoutingError` exception. This also happens when a user
mistypes a URL. However, in such cases, instead of just throwing the exception,
we should inform the user that what they are requesting does not exist by
showing a 404 page.

To handle this, we implemented a `catch_all` route, as the name suggests, which
catches any routes that don't match already defined routes, which will be placed
at the end of our routes configuration. This placement ensures that it is only
called if a request doesn't match any other defined route.

```rb
Rails.application.routes.draw do
  #Define other routes here...

  #Catch-all route for handling errors
  match ":url", to: "errors#show", via: :all, url: /.*/, constraints: -> (request) do
    request.path.exclude?("/rails/")
  end
end
```

This route also uses the same `ErrorsController` mentioned above for handling
unmatched requests and will show a 404 page.

## Links

- [Human page](https://www.bigbinary.com/blog/automatically-displaying-errors)
