🎉

Next.js App Routerでエラー時に500ステータスコードを返せない問題をRoute Groupで解決

に公開

はじめに

NextjsのApp Router使用時にしばしば見受けられる問題、throw new Error()をしても500エラーにならない問題とRoute Groupsでの解決方法を取り上げます。

問題の概要

Next.jsのApp Routerでerror.tsxを使用してエラーハンドリングを実装していますが、Server Componentでthrow new Error()をしても、HTTPステータスコードが200 OKのまま、エラーページが表示されてしまいます。

期待される動作

  • Server Componentでエラーを投げる
  • error.tsxでエラー画面が表示される
  • HTTPステータスコードは500 Internal Server Errorが返される

環境

  • Nextjs 15.x
  • react 19.x
  • app router

解決方法

以下の記事によると、Nextのバージョン15.3.4を使用していると500が返せるようです。
https://github.com/vercel/next.js/discussions/52378

バージョンが違うとはいえ同じNext15系ですが、私の環境では再現できませんでした。
私の場合、問題は layout.tsx にありました。

コード例:

page.tsx
export default function Page () {
    throw new Error("internal server error occurred")
}
error.tsx
export default function Error () {
    return (
        <div>internal server error occurred</div>
    )
}
layout:tsx
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
          <Provider>
              <Header />
                {children}
              <Footer />
           </ Provider>
      </body>
    </html>
  );
}

どうやら、throw new Errorされた時にlayout.tsxProvider,Header,Footerが評価され、ステータスが200となっているようです。
しかしchildrenはエラーのため、error.tsxが表示されていました。

試しにProvider,Header,Footerを削除してみます。

layout.tsx
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        {children}
      </body>
    </html>
  );
}

期待通りに500エラーが返されたことを示すスクリーンショット

うまくいきました。
しかしProvider,Header,Footerを各ページに記述するのは冗長を極めます。

構成の変更

ディレクトリ構成を変更する必要があります。

app/
├── layout.tsx          // ← html/bodyのみ
├── error.tsx           // ← ここでエラーをキャッチ
├── (main)/
│   ├── layout.tsx      // ← Header, Footer, Provider等
│   └── page.tsx
│   └── some-page/
│       └── page.tsx    // ← ここでthrow

このようにlayoutを入れ子にすることでerror.tsx(main)/layout.tsxより外側にあるため、(main)/layout.tsxの評価前にエラーをキャッチできます。
なお、パフォーマンスへの影響はないと考えられます。

なぜこの問題が起きたのかの考察

App Routerはストリーミングレンダリングを採用しており、layout.tsxとページが並列に評価されます。
layout.tsxの出力(<html>タグ等)が先にクライアントに送信されると、その時点でHTTPステータスコード200が確定してしまいます。

HTTPの仕様上、レスポンスボディの送信が始まるとステータスコードは変更できないため、
後からページでエラーが発生しても200のままになったと考えられます。

最後に

ここまでお読みいただきありがとうございました。
今回紹介した方法以外にも解決方法はいくつかあるようです。

  • BEまたはAPI Serverでステータスを明示的に返す
  • pages routerを使用する
  • middlwareを実装する
    などです。

ご自身のプロジェクトにあった解決方法をお試しいただければと思います。
よきエンジニアライフを!

参考

Discussion