Next.jsの本番エラーの原因を特定する|digestとonRequestErrorの活用(2026)
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を安定した検索キーとして使えます。
障害対応時の診断フロー
- ユーザーの報告、スクリーンショット、エラー画面のいずれかからdigestを入手する。
- ユーザーがアクセスしたデプロイを特定する。その後に再デプロイしていれば、まず当時のデプロイのログを探す。
- ランタイムログをdigestで検索する。Vercelのランタイムログは関数の
console出力を記録し、フリーテキスト検索がログメッセージを対象にするため、JSON行に含めたdigestで検索できる。セルフホストならプロセスの出力やログ収集基盤を検索する。 routeTypeを読む。renderはページかレイアウト、actionはServer Action、routeはRoute Handler、proxyはProxy(旧バージョンのMiddleware)を指す。- digestがログにない場合は、
instrumentation.tsの置き場所(直下かsrc/)、アプリ側で例外を握りつぶしていないか、実はクライアント側のエラーではないか、を確認する。 - 修正後は新しいデプロイで同じリクエストを再現し、その
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は含まれない。 - デプロイ前に一時ルートを削除し、外部からエラーを起こせる入口を残さない。
参考資料
- instrumentation.js file convention(Next.js Docs)
- error.js file convention(Next.js Docs)
- Instrumentation guide(Next.js Docs)
- Runtime logs(Vercel Docs)
- System environment variables(Vercel Docs)
- 検証環境:Next.js 15.1.9、React 19.0.0、Node.js 22.16.0、TypeScript strictモード、Windows上で
next buildとnext start、2026-09-29実施
内部リンク
- 親ハブ:Next.js セキュリティ
- 関連記事:
免責事項
本記事は、上記バージョンでのドキュメントとローカル検証にもとづく一般的な技術情報です。digestの生成方法はNext.jsの内部実装であり、リリースによって変わる可能性があります。実際に運用しているバージョンとホスティング環境で動作を確認してください。