Coding in Flow logoGet my free React Best Practices mini course
← Blog

Handling Next.js Route Handler Errors With Higher Order Functions

Oct 15, 2024 by Florian

Featured image

I'm currently working on another Next.js project tutorial for my YouTube channel. One thing that bothered me while setting up the webhook for the Stripe integration was how I had to constantly repeat the error handling code:

export async function POST (req: NextRequest) {

  [...]

  const userId = subscription.metadata?.userId;

  if (!userId) {
    console.log("User ID missing");
    return NextResponse.json({ error: "User ID missing" }, { status: 400 });

  }

  const stripeCustomerId = subscription.customer as string | null;

  if (!stripeCustomerId) {
    console.log("Stripe customer ID missing");
    return NextResponse.json({ error: "Stripe customer ID missing" }, { status: 400 });
  }

  const stripePriceId = subscription.items.data[0].price?.id;

  if (!stripePriceId) {
    console.log("Stripe price ID missing");
    return NextResponse.json({ error: "Stripe price ID missing" }, { status: 400 });
  }

  await prisma.userSubscription.create({
    data: {
      userId,
      stripeSubscriptionId: subscription.id,
      stripeCustomerId,
      stripePriceId,
      currentPeriodEnd: new Date(subscription.current_period_end * 1000),
      cancelAtPeriodEnd: subscription.cancel_at_period_end,
    },
  });

  [...]

}

This is not the final version and I could probably merge this into a single if-check. But it's still annoying that I have to keep repeating the log statement and NextResponse code.

In Express.js, you can create an error handler middleware and put it in front of every endpoint. Here is an example from my Next.js with Express and TypeScript course. It uses the amazing http-errors package to abstract the error message and response code.

I wanted to build something similar in Next.js.

But Next.js middleware is different from Express middleware. We can't plug in an error handler like that. But since it's just JavaScript code, we can create a higher order function.

A higher order function is a function that takes a function as an argument and returns a new function. You can use it to wrap your function into additional code, like error handling code.

So I recreated my Express middleware as a higher order function using the same http-errors package:

import { isHttpError } from "http-errors";
import { NextRequest } from "next/server";

export function handleHttpErrors(
  block: (req: NextRequest) => Promise<Response>,
) {
  return async (req: NextRequest) => {
    try {
      return await block(req);
    } catch (error) {
      console.error(error);
      let status = 500;
      let errorMessage = "Internal server errror";
      if (isHttpError(error)) {
        status = error.statusCode;
        errorMessage = error.message;
      }
      return new Response(errorMessage, { status });
    }
  };
}

As you can see, this function takes in a function that accepts a NextRequest (i.e. a route handler) and returns the same function but wrapped into a try/catch block.

The error handler has a default error code of 500 with a generic message, but it can be customized by throwing a more specific http-error. And this is how you use it:

import createHttpError from "http-errors";

export const POST = handleHttpErrors(async (req: NextRequest) => {  
  [...]
  
  const userId = subscription.metadata?.userId;

  if (!userId) {
    throw createHttpError(400, "User ID missing");
  }

  const stripeCustomerId = subscription.customer as string | null;

  if (!stripeCustomerId) {
    throw createHttpError(400, "Stripe customer ID missing");
  }
  
  const stripePriceId = subscription.items.data[0].price?.id;

  if (!stripePriceId) {
    throw createHttpError(400, "Stripe price ID missing");
  }

  await prisma.userSubscription.create({
    data: {
      userId,
      stripeSubscriptionId: subscription.id,
      stripeCustomerId,
      stripePriceId,
      currentPeriodEnd: new Date(subscription.current_period_end * 1000),
      cancelAtPeriodEnd: subscription.cancel_at_period_end,
    },
  });
  
  [...]
  
}

Now, whenever we need to return an error response, we use the createHttpError function from the http-errors package. We don't have to repeat the console.log or return NextResponse code anymore because it's encapsulated in the error handler function.

It's cleaner and easier to use.

I don't know yet if this approach will make it into the tutorial or if I'll change my mind. You will see in a few weeks 😅

Happy coding!

Florian

Tip: I send regular high-quality web development content to my free email newsletter. It's a great way to improve your skills and stay ahead of the curve.

Subscribe Now

Check out these other posts: