NextTech Insights
Next.jsの本番エラーの原因を特定する|digestとonRequestErrorの活用(2026)
nextjsoperationsloggingobservability

Next.jsの本番エラーの原因を特定する|digestとonRequestErrorの活用(2026)

DigitalCraft
16 min read

Next.jsの本番環境で汎用エラーと識別用の数字(digest)しか表示されない場合に、onRequestErrorで元のエラーを記録し、原因を調べる方法を解説します。Next.js 15での検証結果も紹介します。

目次

Next.jsの本番エラーは、表示されたdigestからどう調べる?

先に結論

本番ビルドのNext.jsは、Server Componentで投げられたエラーのメッセージを汎用の文章に置き換え、代わりに数字の digest だけをクライアントへ渡します。元のメッセージとスタックはサーバー側に残ります。両者をつなぐには、instrumentation.ts で onRequestError をexportし、digest をルートやリクエストパスと一緒にログへ出します。あとはユーザーが見た数字でサーバーログを検索すれば、元のエラーにたどり着けます。

実際にビルドして確かめたところ、運用に効く性質が3つありました。

  • 同じエラーを2回起こすと同じdigestになりました。digestは1回のリクエストではなくエラーの種類を表します。
  • メッセージ内の値を1つ変えるだけで、別のdigestになりました。
  • Route Handlerのエラーは onRequestError に届きましたが、digestは付かず、500レスポンスの本文も空でした。

症状

ローカルでは動くページが本番で落ち、エラーバウンダリ(error.tsx)には自分で投げたメッセージではなく次の英文が表示されます。

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.

error.digest には 4174211443 のような数字が入っています。再読み込みしても同じで、ブラウザのコンソールにもそれ以上の情報はありません。

これは不具合ではなく仕様です。error.js のリファレンスには、Server Componentから転送されたエラーは機密情報の漏えいを防ぐために識別子付きの汎用メッセージになり、error.digest でサーバー側のログと照合できる、と書かれています。一方、Client Componentで投げたエラーは元のメッセージのまま届きます。

検証した内容

Next.js 15.1.9で最小構成のApp Routerアプリを作り、next build と next start で本番モード起動したうえで、次の6リクエストを送りました。

  • /boom?k=a:Server Componentが orders query timed out ... (k=a) を投げる。2回リクエストした。
  • /boom?k=b:同じコードで、メッセージ内の k の値だけが異なる。
  • /api/boom:Route Handlerの GET が例外を投げる。
  • /act:Server Actionが payment provider returned 502 を投げる。
  • /missing:ページが notFound() を呼ぶ。

プロジェクト直下の instrumentation.ts では、エラーごとに1行のJSONを出力しました。

// instrumentation.ts
import type { Instrumentation } from 'next';

export const onRequestError: Instrumentation.onRequestError = async (err, request, context) => {
  const e = err as { message?: string; digest?: string; name?: string };
  console.log(
    JSON.stringify({
      level: 'error',
      source: 'onRequestError',
      digest: e?.digest,
      name: e?.name,
      message: e?.message ?? String(err),
      method: request.method,
      path: request.path,
      routeType: context.routeType,
      routePath: context.routePath,
      renderSource: context.renderSource,
    })
  );
};

サーバー側の出力

next start の出力を抜粋します(スタックトレースは省略)。

{"level":"error","source":"onRequestError","digest":"4174211443","name":"Error","message":"orders query timed out after 5000ms (k=a)","method":"GET","path":"/boom?k=a","routeType":"render","routePath":"/boom","renderSource":"react-server-components"}
 ⨯ Error: orders query timed out after 5000ms (k=a) { digest: '4174211443' }
{"level":"error","source":"onRequestError","digest":"4174211443", ... "path":"/boom?k=a" ...}
{"level":"error","source":"onRequestError","digest":"2392779123", ... "message":"orders query timed out after 5000ms (k=b)" ...}
{"level":"error","source":"onRequestError","name":"Error","message":"webhook signature secret missing","method":"GET","path":"/api/boom","routeType":"route","routePath":"/api/boom"}
{"level":"error","source":"onRequestError","digest":"3242588772","name":"Error","message":"payment provider returned 502","method":"POST","path":"/act","routeType":"action","routePath":"/act","renderSource":"react-server-components-payload"}

クライアントが受け取ったもの

  • Server Component(/boom): HTTP 500。エラーバウンダリには汎用メッセージと digest: 4174211443 が表示され、元のメッセージはHTMLに含まれなかった。
  • Server Action(/act): HTTP 500。RSCペイロードは E{"digest":"3242588772"} だけだった。
  • Route Handler(/api/boom): HTTP 500。本文は空で、digestもなかった。
  • notFound()(/missing): HTTP 404でNot Foundページが返り、onRequestError は呼ばれなかった。

ページのHTMLから元の文字列 orders query も検索しましたが、見つかりませんでした。メッセージはサーバーの外に出ていません。

digestがこう振る舞う理由

Next.js 15.1.9のソース(next/dist/server/app-render/create-error-handler.js)では、digestを持たないエラーに対して err.message + err.stack の文字列ハッシュをdigestとして付けています。検証結果はこれで説明できます。

  • 同じ失敗は同じdigestになる。 メッセージとスタックが同じならハッシュも同じです。digestはエラーをまとめるキーとして扱い、1件のリクエストを指す値だと考えないでください。
  • メッセージ内の可変値でグループが割れる。 IDやタイムスタンプ、クエリ値をメッセージに入れると、値ごとに別のdigestになります。同じ種類のエラーをまとめたいなら、可変データはメッセージではなく構造化フィールドに入れます。
  • 再デプロイをまたぐとdigestは変わりうる。 スタックにはコンパイル後のバンドル内の位置が含まれるため、ビルドが変われば同じバグでもdigestが変わることがあります。実際、instrumentation.ts と error.tsx だけを編集して再ビルドしたところ、変更していない /boom?k=a のエラー位置が page.js:1:4110 から page.js:1:4204 に移り、digestも 4174211443 から 604939349 に変わりました。digestと一緒にデプロイIDかコミットSHAを記録してください。
  • フレームワークの制御フローは除外される。 notFound()、redirect()、動的レンダリングへの切り替えは内部用のdigestを持っており、リクエストエラーとしては報告されません。/missing の結果とも一致します。

このハッシュ方式はドキュメントで保証された仕様ではなく実装の詳細です。Next.jsのメジャーアップデート後は改めて確認してください。

対処:本番で使えるonRequestError

instrumentation.js のリファレンスでは、onRequestError(error, request, context) をサーバー側のエラーを任意の監視サービスへ送るためのフックとして説明しています。Next.js 15.0.0で導入されました。ファイルはプロジェクト直下、src フォルダを使う場合は src/ の中に置きます。リファレンスの注意点のうち、ここで効くのは2つです。フック内の非同期処理は必ずawaitすること。そして error はReactが加工したオブジェクトの可能性があるため、オブジェクトの同一性ではなく digest で識別することです。

そのまま使える形にすると次のようになります。

// instrumentation.ts
import type { Instrumentation } from 'next';

const SAFE_HEADERS = ['user-agent', 'x-request-id', 'x-vercel-id', 'referer'];

export const onRequestError: Instrumentation.onRequestError = async (err, request, context) => {
  const error = err instanceof Error ? err : new Error(String(err));
  const digest =
    typeof err === 'object' && err !== null && 'digest' in err ? String(err.digest) : undefined;

  const headers: Record<string, string> = {};
  for (const name of SAFE_HEADERS) {
    const value = request.headers[name];
    if (value) headers[name] = Array.isArray(value) ? value.join(',') : value;
  }

  const event = {
    level: 'error',
    digest,
    name: error.name,
    message: error.message,
    stack: error.stack,
    method: request.method,
    path: request.path.split('?')[0],
    headers,
    routerKind: context.routerKind,
    routePath: context.routePath,
    routeType: context.routeType,
    renderSource: context.renderSource,
    revalidateReason: context.revalidateReason,
    deployment: process.env.VERCEL_DEPLOYMENT_ID ?? process.env.VERCEL_GIT_COMMIT_SHA,
    runtime: process.env.NEXT_RUNTIME,
  };

  console.error(JSON.stringify(event));

  // Optional: forward to your provider. Await it so the platform does not
  // freeze the function before the request completes.
  // await fetch('https://example.com/errors', { method: 'POST', body: JSON.stringify(event) });
};

設計上のポイントは次の4つです。

  • ヘッダーは許可リストで絞る。 request.headers には cookie や authorization も含まれます。オブジェクトをそのまま外部サービスへ送らないでください。
  • クエリ文字列は落とす。 検索パラメータにはメールアドレスやトークン、IDが入りがちです。パスだけを記録し、必要なパラメータだけを個別に足します。
  • デプロイを識別できるようにする。 digestはビルドごとに変わりうるので、障害対応で実際に使える検索キーは「digest+デプロイ」の組み合わせです。Vercelでは、プロジェクトでシステム環境変数を有効にしていれば VERCEL_DEPLOYMENT_ID と VERCEL_GIT_COMMIT_SHA を実行時に参照できます。セルフホストならビルド時にコミットSHAを注入してください。
  • 軽く、例外を投げない作りにする。 このフックは失敗中のリクエスト上で動きます。送信処理が遅い・落ちるせいで二次障害を起こさないよう、ネットワーク呼び出しにはタイムアウトを付けます。

この版を2回目のテストビルドで動かし、Cookie、Authorization、email クエリパラメータを付けてリクエストしました。記録されたイベントには user-agent、path: "/boom"、routerKind、注入したコミットSHAが残り、3つの機密値はサーバー出力のどこにも現れませんでした。

Node.jsとEdgeの両ランタイムを使う場合は、リファレンスのとおり process.env.NEXT_RUNTIME('nodejs' または 'edge')で分岐し、Node.js専用のSDKをEdge側で読み込まないようにします。

digestでは拾えないケースを補う

Route Handlerは自前のIDを返す

検証ではRoute Handlerのエラーにdigestが付かず、500レスポンスの本文も空でした。これではAPIの利用者が問い合わせに使える値がありません。ハンドラーの境界で例外を捕まえ、ログと同じ相関IDを返します。

// app/api/orders/route.ts
export async function GET() {
  const errorId = crypto.randomUUID();
  try {
    return Response.json(await loadOrders());
  } catch (err) {
    console.error(JSON.stringify({ level: 'error', errorId, message: String(err) }));
    return Response.json({ error: 'Internal error', errorId }, { status: 500 });
  }
}

自分で例外を捕まえると onRequestError にはもう届かないため、この場所でログを出しておきます。

クライアント側のエラー

onRequestError が動くのはサーバー側だけです。ブラウザで発生したエラーは元のメッセージのまま error.tsx に届き、サーバーログには何も残りません。エラーバウンダリから送信し、digestがあれば一緒に含めておくと、サーバー側とクライアント側を1回の検索で突き合わせられます。

'use client';

import { useEffect } from 'react';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Replace with your provider's client SDK.
    console.error({ digest: error.digest, message: error.message });
  }, [error]);

  return (
    <div>
      <p>Something went wrong.</p>
      {error.digest && <p>Reference: {error.digest}</p>}
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

digestを「問い合わせ番号」として画面に出しておけば、サポート担当者がそのままログ検索に貼り付けられます。Next.js 16.3以降では、セグメントを再取得する retry() もエラーバウンダリの安定版propとして渡されます。Next.js 15では reset() を使ってください。

ルートレイアウトのエラー

error.tsx は同じセグメントのlayoutを包みません。ルートレイアウトで落ちたときにも問い合わせ番号を出せるよう、app/global-error.tsx も用意しておきます。

ログの保持期間を先に確認する

digestが役に立つのは、対応するログ行が残っている間だけです。Vercelのランタイムログのドキュメントでは、保持期間はHobbyで1時間、Proで1日(Observability Plusで30日)とされています。翌朝にユーザーからdigestが届いても、該当ログがすでに消えている可能性があります。

問い合わせが保持期間より遅れて届く運用なら、onRequestError のイベントをLog Drainやエラートラッカーへ転送してください。そうすれば、自分たちが管理するシステムの中でdigestを安定した検索キーとして使えます。

障害対応時の診断フロー

  1. ユーザーの報告、スクリーンショット、エラー画面のいずれかからdigestを入手する。
  2. ユーザーがアクセスしたデプロイを特定する。その後に再デプロイしていれば、まず当時のデプロイのログを探す。
  3. ランタイムログをdigestで検索する。Vercelのランタイムログは関数の console 出力を記録し、フリーテキスト検索がログメッセージを対象にするため、JSON行に含めたdigestで検索できる。セルフホストならプロセスの出力やログ収集基盤を検索する。
  4. routeType を読む。render はページかレイアウト、action はServer Action、route はRoute Handler、proxy はProxy(旧バージョンのMiddleware)を指す。
  5. digestがログにない場合は、instrumentation.ts の置き場所(直下か src/)、アプリ側で例外を握りつぶしていないか、実はクライアント側のエラーではないか、を確認する。
  6. 修正後は新しいデプロイで同じリクエストを再現し、その routePath で同じメッセージが出なくなったことを確認する。同じバグでもビルドが変わればdigestは変わりうるため、新旧ビルドのdigestを比べて判断しない。

5分で設定を検証する

例外を投げる一時的なルートを追加し、本番モードでビルドして両側を見比べます。

npm run build && npm run start
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/debug-throw

合格条件は次のとおりです。

  • ブラウザには自分のメッセージではなく、汎用メッセージとdigestが表示される。
  • サーバーには同じdigest、元のメッセージ、routeType: "render" を含む onRequestError の行が1行出て、Cookieは含まれない。
  • デプロイ前に一時ルートを削除し、外部からエラーを起こせる入口を残さない。

参考資料

内部リンク

免責事項

本記事は、上記バージョンでのドキュメントとローカル検証にもとづく一般的な技術情報です。digestの生成方法はNext.jsの内部実装であり、リリースによって変わる可能性があります。実際に運用しているバージョンとホスティング環境で動作を確認してください。

人気記事

  1. 1Permit2とは何か?Approveが変わった理由と安全な使い方(チェックリスト)
  2. 2署名(Sign)画面の読み方|Approve/Permit/NFT全権限を30秒で判別するチェックリスト
  3. 3仕様→実装に落とすプロンプトテンプレ(AI開発)|AIに“迷わせない”仕様の書き方
  4. 4EVMのトークン承認(Approve)を見直す方法|Revokeの手順と判断基準(チェックリスト)
  5. 5曖昧な依頼を要件定義に変換する質問リスト(AI開発)|AIに実装させる前に聞くべきこと

関連記事