【Next.js】error.tsxを使ったエラーハンドリングでハマった

## やろうとしていたこと
サーバーサイドの処理で、エラーが発生した場合、エラーのステータスに応じて、エラー画面を出し分ける。
https://nextjs.org/docs/app/building-your-application/routing/error-handling

## 環境
```json
"next": "14.2.11",
"react": "18.3.1",
"typescript": "5.6.3"
```
※App router使用

## 起きていた事象

最初は、こんな感じで実装していました。
devモードではこれで問題なくステータスによって出し分けできていましたが、prodモードで確認したところ、うまく動作しませんでした。( `if (error instanceof CustomError)`に不一致でswitch文に流れていなかった。)

```tsx
//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>;
}
```

```typescript
//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;
  }
}
```

```tsx
//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までエラーを渡します。
1. server sideのエラーをserver sideで捕捉(catch)
2. 捕捉したエラーを普通のオブジェクトに変換
3. 変換したオブジェクトをclient side(component)に渡す
4. client sideでエラーを元の型(カスタムエラー)に戻して再度throwする
5. error.tsxはclient side renderingなので、問題なくカスタムエラーを受け取れる

```typescript
//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;
};
```

```tsx
// 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()} />;
    }
  }
}
```
```tsx
// 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);
};
```
```tsx
// 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