やろうとしていたこと
サーバーサイドの処理で、エラーが発生した場合、エラーのステータスに応じて、エラー画面を出し分ける。
https://nextjs.org/docs/app/building-your-application/routing/error-handling
環境
"next": "14.2.11",
"react": "18.3.1",
"typescript": "5.6.3"
※App router使用
起きていた事象
最初は、こんな感じで実装していました。
devモードではこれで問題なくステータスによって出し分けできていましたが、prodモードで確認したところ、うまく動作しませんでした。( if (error instanceof CustomError)
に不一致でswitch文に流れていなかった。)
//src/app/error.tsx
"use client";
export default function ErrorPage({ error }: { error: CustomError }) {
if (error instanceof CustomError) {
switch (error.statusCode) {
case 400:
return <p> Bad Request</p>;
case 500:
return <p>Internal Server Error</p>;
case 503:
return <p>Service Unavailable</p>;
default:
return <p>Error</p>;
}
}
return <p>error</p>;
}
//src/app/custom-error.ts
import { CustomError } from "./custom-error";
export class CustomError extends Error {
statusCode: number;
constructor(error: { statusCode: number; message: string }) {
super();
this.statusCode = error.statusCode;
this.message = error.message;
}
}
//src/app/page.tsx
import { CustomError } from "./error";
export default function Home() {
throw new CustomError({
message: "Service Unavailable",
statusCode: 503,
});
}
原因
prodモードでerror.tsxに渡されたerrorをデバッグしてみると、このような文字列になっていました。
Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
※訳エラーが発生しました: Server Components のレンダーでエラーが発生しました。具体的なメッセージは、機密情報の漏えいを防ぐため、プロダクションビルドでは省略されます。このエラーインスタンスにはダイジェストプロパティが含まれています。
との内容で、devモードでは問題ないですが、エラーオブジェクトをclient sideに出すと情報漏洩の観点でまずいので、そういった仕様になっているようです。
解決方法
サーバー側からErrorオブジェクトしては直接client sideに渡すことはできないので、下記の手順でerror.tsxまでエラーを渡します。
- server sideのエラーをserver sideで捕捉(catch)
- 捕捉したエラーを普通のオブジェクトに変換
- 変換したオブジェクトをclient side(component)に渡す
- client sideでエラーを元の型(カスタムエラー)に戻して再度throwする
- error.tsxはclient side renderingなので、問題なくカスタムエラーを受け取れる
//src/app/custom-error.ts
export type APIErrorObject = {
message: string;
errorCode: number;
};
export class CustomError extends Error {
errorCode: number;
constructor(props: APIErrorObject) {
super(props.message);
this.errorCode = props.errorCode;
}
serialize(): SerializedCustomError {
return {
message: this.message,
errorCode: this.errorCode,
};
}
static deserialize(data: SerializedCustomError) {
return new CustomError({
message: data.message,
errorCode: data.errorCode,
});
}
}
export type SerializedCustomError = {
message: string;
errorCode: number;
};
// src/app/page.tsx
import { ThrowDeserializedCustomError } from "@/client-components/client-component";
import { CustomError } from "./custom-error";
export default function Home() {
try {
// なんらかの処理で例外発生
throw new CustomError({
message: "Service Unavailable",
errorCode: 400,
});
} catch (e) {
// サーバーサイドで捕捉
if (e instanceof CustomError) {
// ThrowDeserializedCustomErrorはclient component
// ここにserializedされたエラーオブジェクトを渡す
return <ThrowDeserializedCustomError serializedError={e.serialize()} />;
}
}
}
// src/client-components/client-component.tsx
"use client";
import { SerializedCustomError } from "@/app/custom-error";
import { CustomError } from "../app/custom-error";
export type Props = {
serializedError: SerializedCustomError;
};
export const ThrowDeserializedCustomError = ({ serializedError }: Props) => {
// client sideから再度throwする
throw CustomError.deserialize(serializedError);
};
// src/app/error.tsx
"use client";
import { CustomError } from "./custom-error";
export default function ErrorPage({ error }: { error: CustomError }) {
// client side(ThrowDeserializedCustomError)からthrowされたエラーが渡される
if (error instanceof CustomError) {
switch (error.errorCode) {
case 400:
return <p> Bad Request</p>;
case 500:
return <p>Internal Server Error</p>;
case 503:
return <p>Service Unavailable</p>;
default:
return <p>Error</p>;
}
}
return <p>error</p>;
}
参考
https://zenn.dev/nabeliwo/articles/02f8cfcc596bb9
https://qiita.com/__ttsubasa__/items/a1fd399dd490e7402518
https://zenn.dev/link/comments/8a78d3c89fdabe
https://dev-harry-next.com/frontend/nextjs-app-router-error-handling#hf07ff9d3b8