Wordle in Remix: Part 10 - Error Handling

Wordle in Remix: Part 10 - Error Handling


5 min read

This is the tenth and last article in a series where we create a Wordle clone in Remix! 💿 We went step by step, from setting up the application to creating pages and implementing game logic. In this article, we will handle any unknown errors that might be thrown in our applications and prepare views to display something meaningful to the users. Let's go!

Catching unforeseen errors

Currently, our application is relatively simple, so there should be no errors happening other than the ones we've recently covered with validation. But in real applications, that deal with more network requests or have third-party code, we cannot have this certainty. In either case, it's useful to have a safety net in place to catch any errors that we haven't explicitly handled so far.

Error boundaries

Let's see what would happen if code in any of our roots throws. We can simulate that case by adding an error somewhere in the code.

// app/routes/play.tsx
export default function Play() {
  throw new Error("Boom! 💥");

We get the following in the browser.

Default Error Boundary

We can improve this using an Error Boundary. You might be familiar with this concept from React applications. Error Boundary is a wrapper component that catches errors that are thrown by its children (or at least most of the errors). If it catches one, it takes over and renders a fallback UI that you specified.

That concept is baked straight into Route Module API in Remix. You can export the ErrorBoundary function and when an error is caught, Remix will render the closest error boundary.

What you currently see is the default root error boundary. As with your route's views, you can define error boundaries on any level you want - the root error boundary will replace the entire view, and any nested error boundary will only replace the part of the view that's handled by a given route.

In our case, let's create an error boundary just for the /play route.

// app/routes/play.tsx
import { ErrorBoundaryComponent } from "remix";
export const ErrorBoundary: ErrorBoundaryComponent = ({ error }) => {
  return (
    <main className="p-12">
      <div role="alert" className="rounded-md p-4 bg-red-100 ">
        <h2 className="font-bold text-lg">Oh no!</h2>
        <p>Something went wrong.</p>
        <pre className="mt-4">{error.message}</pre>

With that, we get the following.

Custom Error Boundary

As you see, our boundary replaced only the part of the view that the route was handling - we still render the header, since it's rendered on the root level.

That's how we can use error boundaries to handle unexpected errors. 🥳

Page not found

The last thing that we will add to our application is a custom 404 page. Remix by default gives us a simple page for handling 404 - you might see that by entering any URL we don't have a module for.

Default 404 Page

There are two most common ways to implement custom 404 in Remix.

  • Using a Catch Boundary - this is another part of Route Module API that allows handling error responses that are thrown on the server-side.
  • Using a splat route - this is a route module that handles any URL that there are no other route matches.

They have slightly different characteristics and lend themselves to be used in slightly different scenarios. For what we need, we could use either one - since the first one is already covered by docs, we'll use the second one.

Splat routes

You can create a splat route by naming your module $.jsx - this will catch any otherwise unmatched route from that point onwards.

// app/routes/$.tsx
import { Link } from "remix";
import { styles as buttonStyles } from "~/components/Button";

export default function Fallback() {
  return (
    <main className="text-center flex justify-center">
      <div className="max-w-lg">
        <h1 className="text-4xl font-bold mt-16 mb-4">
          Something's missing...
        <p className="mb-6 text-lg">
          We couldn't find the page you were looking for. Maybe it was never
          here in the first place.
        <Link to="/" className={buttonStyles({ size: "lg" })}>
          Go home

With that, we get the following in the browser.

Custom 404 Page

Since we created the file in the app/routes directory, we still get whatever is rendered in the root module - the header. We're really close here, but there's one thing missing. Let's take a look at the browser's network tab when we enter that page.

Network Tab for 404 Page

Our server returns status 200 OK which is not exactly what we would expect there. 🙃

Loaders to the rescue

Since a splat route is just another Remix route we have the access to all standard module functions. Let's create a loader for this page, where we will be able to control that.

// app/routes/$.tsx
import { LoaderFunction } from "remix";
export const loader: LoaderFunction = async () => {
  return new Response("Not Found", {
    status: 404,

When we now take a look into the console, we should get the following.

Network Tab with a Proper Status For 404

Sweet! We have our custom 404 and we return a proper HTML Response status when this page is served. 👌 This also gives you a way to load data for your 404 page.


That is - we've just added the last pieces around error handling. ✨ Here's a short summary of things we've gone through.

  • discussed the concept of Error Boundaries in React applications
  • implemented Error Boundary component
  • outlined possible ways to approach 404 pages
  • added a custom 404 page in our application using a splat route with a loader function

Congratulations! 🥳 I don't know about you, but it's been a heck of a ride! We've just reached the end of the series where we created a Wordle clone in Remix. We've touched on a lot of functionalities that Remix brings to the table, but there's still more to it. I hope you found this useful and that you learned something that you will use in your applications in the future. 🚀

If you've build your own Wordle clone or any other application, make sure to share that - I'd love to see what you came up with. Keep learning and keep exploring! 🧑‍💻

If you liked the article or you have a question, feel free to reach out to me on Twitter‚ or add a comment below!

Further reading and references