Reputation: 6471
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
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.
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
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
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