Remix Error Handling Essentials

Updated 31. Jul 2023

As I’m planning to work on other Remix projects in the future, I wanted to write this blog post as guide for myself. The goal of this blog post is to cover error handling and tracking, so that if you follow it from the beginning to the end, your errors will give a good experience to the users and you will learn from the errors. It’s a work in progress and I’ll be updating it as I learn new things.

Getting Started

Before diving into the nitty-gritty details of error handling in Remix, it's important to note that this guide assumes you have a general understanding of errors and are using the latest Remix features, specifically v2_errorBoundary. If you're not already familiar with error boundaries, I highly recommend checking out the Remix documentation for more information.

TLDR:

Here is what this article covers in short:

  • Define a 404 page: One of the most common errors in all web page applications is the 404 Not Found error. To handle this error in Remix, you'll need to create a new file $.tsx at the root of your routes folder and respond with 404 in a loader. Then, define your page using a simple component to show the error. Don’t track 404s.

  • Define a Root Error Boundary: This is a last resort error boundary that will be triggered for errors when no other error boundaries exist. Track those errors.

  • Customize handleError: Remix defines handleError by default to simply forward the error to console.log. To customize how errors are handled, you can export your own handleError function in entry.server.ts.

  • Track errors: Consider using an error tracking service like Sentry to track errors in your Remix project.

  • Provide clear feedback: Whenever you show an error, give your users guidance on what went wrong and how to resolve it. Don't just show "Something went wrong." Instead, show something like "Page Not Found. Go Home."

Define 404

First let’s cover one of the most common errors in all web page applications, 404 Not Found.

There are a couple of things you need to do:

  • Create a new file $.tsx at the root of your routes folder This is effectively a catch-all for URLs that do not find their route.

  • Respond with 404 in a loader Remix doesn’t know yet that you are using this catch-all route to handle 404 errors, so it won’t respond with http status code of 404 as search engines would expect. So add a loader and respond with 404.

export function loader() {
  return new Response("Not Found", {
    status: 404,
  });
}
  • Define Your Page: I’m using a simple component to show the error, but if you want you can make your page nicer.

export default function NotFoundPage() {
  return <BigStatusMessage
    type="error"
    message="Not Found"
    title="404"
   />;
}

Define Root Error Boundary

This is a last resort error boundary and it will be triggered for errors when no other error boundaries exist. You should have more than just Root Error Boundary.

// root.tsx
export function ErrorBoundary() {
  const error = useRouteError();
  return (
    <html>
      <head>
        <title>Oh no!</title>
        <Meta />
        <Links />
      </head>
      <body>
        {/* add the UI you want your users to see */}
        <Scripts />
      </body>
    </html>
  );
}
ℹ️

Use isRouteErrorResponse to check if it’s an HTTP Error response. You can use it to check error.status for example.

isRouteErrorResponse will return true for Responses thrown in loaders and actions. Note how in our 404 page loader we are simply returning the response instead of throwing it?

Handle Other Errors

There has been a lot written about how and where to put other error boundaries, so I'll keep this light. But my rule of thumbs are:

  • You don’t need ErrorBoundary for every scenario

  • Wherever you define <Outlet /> , you should likely define the boundary

ℹ️

Keep in mind that ErrorBoundary will be rendered on the server only if there is a loader error. If there is an action error, then it is rendered on the client only. Why does this matter? If you are naively capturing and tracking errors in a boundary, you will need to implement some logic to determine the type of error.

Don't throw input errors

First of all, you don’t want to handle input errors with an ErrorBoundary. You ideally want to handle them next to user’s input.

Secondly, throwing errors will trigger handleError and if you’ve set up tracking (as described below) you will get all of your input errors reported to your error tracking service. I promise you, you don’t need to know when somebody forgot to enter a valid email, and if you really need it, it should probably be part of analytics.

Track Errors

Error tracking provides valuable insight into how users are interacting with the application, which helps developers improve the user experience and prioritize bug fixes. By tracking errors, you can also identify patterns and trends in errors, allowing for more efficient and effective debugging.

This is the key to having a stable application.

Define HandleError

Remix defines handleError by default to simply forward the error to console.log. To customize how errors are logged/tracked, you can export your own handleError function in entry.server.ts

export function handleError(
  error: unknown,
  { request, params, context }: DataFunctionArgs
): void {
  // Track the error somehow
  console.log(error);
}
ℹ️

handleError is called whenever your server-side code throws an error, including errors that are already handled with ErrorBoundary.

Extend Your Error Boundaries with Error Tracking

One thing to keep in mind is that you can't just blindly track all errors in ErrorBoundaries. If you are using Sentry it's fairly simple to track errors at boundaries, you simply call captureRemixErrorBoundaryError. But if you are not using Sentry, you will have to implement some logic, so your errors are not duplicate. For example, since handleError gets called for every error not matter the depth of the error boundary, you need some logic to exclude certain events.

I recommend you have a look at how Sentry deals with it in this piece of code.

Tracking With Sentry

If you are using Sentry, their official guide will get you pretty far, but not quite to the finished integration. Here are the missing steps from Sentry’s integration:

Extend handleError so you keep logging errors

Their docs don’t mention this, but basically the snippet in their docs does not include console.log. This will affect both your development and production environment. Meaning your errors won't e logged and may go unnoticed by the team.

So don't forge to add console.log to handleError !

Fix Server Side Source Maps

⚠️

Sentry Team is aware of the issue, you can check this GitHub issue to see if it was resolved already and skip this chapter.

If you follow the guide you will notice that you have to upload the source maps to Sentry, so you can see more than just minified stack trace. Their docs do a good job explaining how to set up the basics, and it works for client side errors.

Since server has a separate bundle in Remix, it has a separate set of source maps as well, but their sentry-upload-sourcempas script only uploads client side sourcemaps. The fix is quite easy:

  • Enable sourcemap generation for your server side remix projects. If you’ve used indie stack this involves adding a --sourcemap flag the following 2 build scripts in package.json:

"build:remix": "remix build --sourcemap",
"build:server": "esbuild --platform=node --sourcemap --format=cjs ./server.ts --outdir=build --bundle",
  • Upload the server side sourcemaps immediately after sentry-upload-sourcemaps uploads the client side. For example:

yarn sentry-upload-sourcemaps --release $SENTRY_RELEASE
yarn sentry-cli releases files "$SENTRY_RELEASE" upload-sourcemaps \
  ./build \
  --dist "server" \
  --url-prefix '/usr/server/app/remix/build'

I am not sure --dist is required, but it works for me, so I’m keeping it for now. Note that due to the --dist flag you also need to update Sentry.init({ dist: 'server', ...})

  • Remove the sourcemaps before shipping the production bundle. This is a security precaution.

# Remove .map files
find ./public/build -type f -name '*.map' -delete && \
find ./build -type f -name '*.map' -delete && \

# Strip sourcemap links from source files
find ./public/build -type f -name '*.js' -exec sed -i -e '/\/\/# sourceMappingURL.*/d' {} + && \
find ./build -type f -name '*.js' -exec sed -i -e '/\/\/# sourceMappingURL.*/d' {} +

Notice how we strip source map links from source files? That’s because otherwise every time you open the dev tools, your browser will try to request those files, resulting in a bunch of not found errors. You don’t want to have your console polluted with warnings and you don’t want those errors reported in sentry.

A Piece of advice for UX

Whenever you show an error give your users guidance on what went wrong and how to resolve it.

For example, don’t just show “Something went wrong”. Instead show something like: “Page Not Found. <Go Home>”

Error UX Suggestion. Always provide escape hatch.

TaDa!

If you've made it this far, hopefully your error handling is in a better shape now. If you have any suggestions let me know on Twitter. Would love to learn from the community.

And because I love learning from a community, I started a unique "community driven" newsletter. Join now below👇 or learn more.