Using Error Boundaries in Remix to Prevent Broken Pages on Client-Side-Only Errors

  You block advertising 😢
Would you like to buy me a ☕️ instead?

Client-side errors can occur for various reasons, such as unsupported JavaScript features, network issues, or browser extensions that interfere with JavaScript execution. Progressive enhancement helps us build web applications that remain functional even under these unfavorable conditions. However, the default error handling in Remix breaks our progressive enhancement efforts when client-side errors occur.

Error handling is essential when developing web applications to let users know why and what is going wrong and ensure a smooth user experience. In React applications, error boundaries provide an excellent way to catch errors and display fallback UIs when something goes wrong. In addition, the Remix meta-framework supports progressive enhancement out of the box, allowing it to do some magic to handle client-side-only errors (e.g., because of errors in an unsupported browser) without breaking the core functionality.

In this article, we’ll explore using error boundaries in the Remix framework to prevent broken pages due to client-side-only errors. First, we’ll dive into the default behavior of Remix when a client-side error occurs and explain why it can be problematic. Finally, we’ll implement a custom error boundary in Remix that ensures our pages remain functional even when client-side JavaScript fails to execute.

// Regular `ErrorBoundary` not preventing problems
// with client-side-only errors.
export function ErrorBoundary({ error }) {
  return (
    <div>
      <h1>Error</h1>
      <p>{error.message}</p>
      <p>The stack trace is:</p>
      <pre>{error.stack}</pre>
    </div>
  );
}

Progressive enhancement and Remix

Progressive enhancement is a design principle emphasizing the importance of building web applications that work for every user, regardless of their browser, device, or network conditions. This approach starts with basic HTML and CSS, ensuring that the core functionality is accessible to everyone. Then, we add enhancements through JavaScript to improve the user experience for those with modern browsers and capable devices.

Remix is a meta-framework built on top of React that embraces progressive enhancement. It works by not only rendering content on the client side, like regular SPAs but also server side, which ensures that our application works even if the user’s browser does not support some of the JavaScript features we use, has JavaScript disabled, or it fails to load. The server-side rendering (SSR) approach delivers a fast, fully functional experience to users. At the same time, client-side hydration (running JavaScript in the browser) enhances the user experience with interactivity and performance optimizations.

By leveraging the progressive enhancement approach, Remix applications can provide an inclusive experience for every user, regardless of their environment. Unfortunately, the way error handling is set up in Remix by default breaks our web app needlessly if a client-side error occurs, even if the server-side rendered application would be sufficient for the user to achieve their goal. In this article, we’ll explore how we can improve upon this suboptimal default error handling.

Default behavior in Remix for client-side errors

As we’ve seen, Remix supports progressive enhancement out of the box, allowing our application to function even if JavaScript fails to execute client-side. However, when it comes to handling client-side errors, the default behavior of Remix can create undesirable results.

When a client-side error occurs in a Remix application, the framework generates error HTML that overrides the server-side rendered regular HTML. Overriding the server-side HTML means that even if the server-side rendering is working correctly and providing the necessary content and functionality, a client-side error can cause the page to break, displaying the error HTML instead of the expected content.

This default behavior can lead to a poor user experience. For example, a single client-side error can render the entire page unusable, even though the server-side rendered version of the application would still be functional - essentially breaking our progressive enhancement efforts. In the following sections, we’ll explore how to implement a custom error boundary to address this issue and prevent broken pages due to client-side errors.

Implementing a custom ErrorBoundary in Remix

Now that we understand the default behavior of Remix when a client-side error occurs let’s create a custom error boundary to ensure our application remains functional despite client-side JavaScript issues.

The following code example demonstrates a custom ErrorBoundary component in Remix. Let’s break it down step by step to understand how it works:

// root.tsx
import { createElement } from "react";
import {
  Links,
  LiveReload,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";

const isClient = typeof document !== "undefined";

// Custom `ErrorBoundary`.
export function ErrorBoundary() {
  if (isClient) {
    return createElement("html", {
      suppressHydrationWarning: true,
      // Inject the server-side rendered HTML.
      dangerouslySetInnerHTML: {
        __html: document.getElementsByTagName("html")[0].innerHTML,
      },
    });
  }

  // Server-side rendered error page.
  return (
    <html lang="en">
      <head>
        <title>Oh no!</title>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>{/* add the error UI you want your users to see */}</body>
    </html>
  );
}

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

For the isClient variable we check if the document object is not undefined, which indicates that the code is running on the client side. If it is, the error boundary returns the original server-side rendered HTML, preventing the error HTML from overriding it.

If the document object is unavailable (e.g., during server-side rendering), the error boundary returns a custom UI that developers can adapt to display helpful information to users.

By implementing and using this custom error boundary in our Remix application, we can prevent broken pages due to client-side errors and ensure that the server-side rendered application remains functional to users even if the execution of client-side JavaScript fails.

Wrapping it up

Keeping the server-side rendered HTML accessible even if an error happens on the client provides a better user experience. In addition, it can have practical implications on your bottom line, such as guaranteeing a webshop checkout process does not fail, thereby preventing potential revenue loss.

In summary, using error boundaries in the Remix framework is a powerful technique to prevent broken pages due to client-side errors. Therefore, I encourage you to implement the custom error boundary in your Remix projects and enjoy the benefits of a more resilient application.


Do you want to learn how to build advanced Vue.js applications?

Register for the Newsletter of my upcoming book: Advanced Vue.js Application Architecture.



Do you enjoy reading my blog?

You can buy me a ☕️ on Ko-fi!

☕️ Support Me on Ko-fi