CCCC
CCCC

Reputation: 6471

NextJS - type issue of Error handling in Route Handler

I have a route handler like below

app/api/transactions/route.ts

import { NextRequest, NextResponse } from "next/server";
import { AxiosError } from "axios";
import axios from "../axios";
import { AxiosResponse } from "axios";
import { Transaction } from "../../../types";

interface Error {
  message: string[];
  statusCode: number;
}

export async function GET(request: NextRequest) {
  try {
    const transactionsRes: AxiosResponse<Transaction[]> = await axios.get(
      `/transactions`
    );

    return NextResponse.json(transactionsRes.data);
  } catch (error) {
    const axiosError = error as AxiosError<Error>;
    return NextResponse.json(
      { error: axiosError?.response?.data.message },
      { status: axiosError?.response?.data.statusCode }
    );
  }
}

MyComponent.tsx

export const MyComponent = async() => {
  const raw = await fetch('api/transaction');
  const transactions: Transaction[] = await raw.json();
  if (transactions.error) {
    throw new Error(transactions.error);
  }
  ...
}

I also have a error.tsx served as a error boundary to catch error

The problem I found is that, when I handle error in this way, I might need to type the transaction response like this.

const transactions: Transaction[] | TransactionError = await raw.json();

The type seems so confusing and not extendable to me if I do in this way. Just wondering what is the best structure to handle errors in nextjs?

Upvotes: 0

Views: 351

Answers (1)

Arash Jahan Bakhshan
Arash Jahan Bakhshan

Reputation: 1170

if the error's data is included in the response body as json, the only way to get it is raw.json(), so there would be no magical way to handle it. Your way is correct but I can show you some tips to use the data.

1. Type-gaurd (narrowing)

You can check the properties and typeof if the data to divide them completely type-friendly like this:

if(Array.isArray(transactions)) {
    transactions.map(() => {}) // works fine and type-friendly
} else {
    transactions.errorMessage // works fine and type-friendly
}

Or even cleaner if you want to do this in many places:

function isTransactionError(data: unknown) data is TransactionError {
    return data && data.errorMessage
}

if(isTransactionError(transactions)) {
    transactions.errorMessage // works fine
} else {
    transactions.map(() => {}) // works fine
}

You can read more about type-guards here: https://www.typescriptlang.org/docs/handbook/2/narrowing.html

2. Put your data and errors in separate properties (if backend is yours)

You can return the data and error objects in separate properties. Most of the frameworks and API tools such as graphql do the same thing.

// success:
{
    data: [{...}, {...}], // here is your transactions
    error: null
}

// failure:
{
    data: null,
    error: {...}
}

then you can type-guard this as well:

result.data?.map(() => {})

result.error?.errorMessage
3. Another way to check if the response is error (raw.ok)

Mozilla says here:

The ok read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not.

You can use it this way:

if(raw.ok) {
    console.log("Success!")
} else {
    console.log("Error :(")
}

However, it is not type-friendly. So let's make it:

type ResponsePayload<TResult, TError = unknown> = {
    data: TResult | null;
    error: TError | null;
}

type ResponseWithPayload<TResult, TError = unknown> = Response & {
    payload: ResponsePayload<TResult, TError>
}

async function withTypes<TResult, TError = unknown>(response: Promise<Response> | Response) {
   const resp = await response as ResponseWithPayload<TResult, TError>;

   const payload = await resp.json();

   if(resp.ok) {
      resp.payload = {
         data: payload,
         error: null,
      }
   } else {
      resp.payload = {
         data: null,
         error: payload,
      }
   }

   return resp;
}

And use it like this:

// example 1
const resp = await withTypes<Transaction[], TransactionError>(fetch("..."))

resp.payload.data?.map(() => {});
resp.payload.error?.errorMessage;

// example 2

const resp2 = await fetch("...");
const { payload } = await withTypes<Transaction[]>(resp2)

payload.data?.map(() => {});

Upvotes: 1

Related Questions