Next.jsのよくあるエラー図解——何が起きているか・直し方・防ぎ方
2026-07-31
ChatGPTやClaudeにNext.jsのコードを書かせる人が増えて、同じエラーで止まる人も増えた。仕事柄、AIが書いたコードの診断をしていると、持ち込まれるエラーはだいたい同じ顔ぶれに収束する。
この記事では頻出6種を、「何が起きているか(図解)」→「直し方」→「防ぎ方」の順で並べる。先に種明かしをすると、6つのうち5つは同じ一つの事実から生えている。
Next.jsのコードには「サーバーで実行される部分」と「ブラウザで実行される部分」の2つがあり、その境界をまたいだときにエラーが出る——これだけ。エラーメッセージは違っても、根っこは共通している。
対象はNext.js App Router。執筆時点(2026年7月)の安定版はNext.js 16.2系で、記事もそれを前提にする。バージョンで挙動が変わる箇所(特にキャッシュ)はその都度書く。
1. Hydration failed(画面が一瞬正しく出るのに赤いエラー)
症状:開発画面に Hydration failed because the server rendered HTML didn't match the client. や Text content does not match server-rendered HTML. が出る。表示は概ね動いているように見えるので、初見だと何が悪いのか分からない。
何が起きているか:Next.jsはまずサーバーでHTMLを生成して送り1、ブラウザ側でReactが同じ内容をもう一度組み立てて、送られてきたHTMLと突き合わせる。この照合作業がハイドレーション2。両者は「完全一致する」前提なので、1文字でも違うとエラーになる。
サーバーとブラウザで結果が変わる書き方をしていると起きる。公式が挙げる主な原因はこのあたり:
new Date()など、実行するたびに値が変わるものをそのまま表示している(サーバーで9時59分、ブラウザで10時0分になれば不一致)typeof window !== 'undefined'の分岐で表示を変えている(サーバーとブラウザで違う枝を通る)localStorageやwindowの値を初回レンダリングで使っている- HTMLのネストが不正(
<p>の中に<div>や<p>、<a>の中に<a>など。ブラウザが構造を勝手に補正してズレる) - ブラウザ拡張機能やCDNの最適化機能がHTMLを書き換えている(自分のコードが悪くないパターン)
直し方:
- 時刻・乱数・localStorage由来の表示は、初回レンダリングでは出さず
useEffectの中でセットする(useEffectはブラウザでしか走らないので、照合が終わった後に安全に差し替わる) - そのコンポーネント自体をサーバーで描く意味がないなら
next/dynamicのssr: falseでクライアント専用にする - タイムスタンプ表示など「違って当然」の箇所だけ、その要素に
suppressHydrationWarningを付ける(乱用は禁物。公式も1階層だけの逃げ道としている) - HTMLネスト起因なら、エラーメッセージ内に問題のタグが出るので素直に構造を直す
防ぎ方:「この値、サーバーで実行しても同じになるか?」を初回表示に使う値すべてに問う。AIにコードを書かせるときは「現在時刻はuseEffectでセットして」のように非決定的な値の扱いを指示に含めると踏みにくい。
2. window is not defined(ビルドや初回アクセスで即死)
症状:ReferenceError: window is not defined(document や localStorage でも同じ)。ページを開いた瞬間、あるいは next build の最中に落ちる。
何が起きているか:エラー1と同族で、こちらはより直接的。window はブラウザにしか存在しないオブジェクトなのに、サーバー(Node.js)で実行されるタイミングのコードで触った。コンポーネントの本体やモジュールの先頭に書いたコードは、サーバーでも実行される。
直し方:
- ブラウザ専用の処理は
useEffect内へ移す(サーバーでは実行されない) - ブラウザ専用ライブラリ(地図、チャート、エディタ系に多い)は
next/dynamic+ssr: falseで読み込む - どうしても分岐が必要な場所では
typeof window === 'undefined'でガードする——ただし表示内容の分岐に使うとエラー1(hydration mismatch)に化けるので、処理のガード専用にする
防ぎ方:「windowを触るのはuseEffectの中だけ」をルール化する。AI生成コードではモジュール先頭で const width = window.innerWidth のようなコードが平気で出てくるので、レビュー時はファイル先頭とコンポーネント直下を先に見る。
3. useState only works in a Client Component("use client"の境界)
症状:開発画面に長文のエラーが出る。
You're importing a component that needs useState. It only works in a
Client Component but none of its parents are marked with "use client",
so they're Server Components by default.
あるいは実行時に Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".
何が起きているか:App Routerでは、コンポーネントは既定で全部Server Component3。サーバーでだけ実行され、ブラウザにはJSが配られない。useState や onClick はブラウザで動く機能なので、使いたいファイルには "use client" を宣言して「ここから下はブラウザにも配る」と境界を引く必要がある。
もう一つの「Functions cannot be passed」は境界の性質によるもの。サーバーからクライアントへ渡すpropsは、一度シリアライズ(文字列化して転送できる形に変換)される。文字列・数値・配列・プレーンなオブジェクトは渡せるが、関数は変換できないので渡せない。
直し方:
useState/useEffect/onClickを使うファイルの先頭に"use client";を書く- ただしpage.tsxの先頭に書くのは最後の手段。そこに書くとimportしている部品が芋づる式に全部クライアント行きになる。ボタンや入力欄など「操作がある部分」だけを小さいコンポーネントに切り出して、そこにだけ付けるのが定石
- 関数を渡そうとしてエラーになった場合は、渡す設計をやめてClient Component側で定義するか、サーバーで実行したい処理なら Server Functions(
"use server")にする
防ぎ方:「ページの大枠はサーバー、操作する部品だけクライアント」という分担を先に決めてから書く。AIへの指示なら「インタラクティブな部分は別コンポーネントに分けて"use client"を付けて」と一言足すだけで、page全体クライアント化の雑なコードが減る。
出典: Next.js公式: client hook in server component / Next.js公式: Server and Client Components / React公式: "use client"
4. Module not found: Can't resolve 'xxx'
症状:Module not found: Can't resolve 'fs' のようなビルドエラー。fsのほかdns、netなどでも出る。あるいは「手元のMacでは動くのに、デプロイすると Can't resolve './Header' で落ちる」。
何が起きているか:2パターンある。
- Node.js専用モジュールがブラウザ用のコードに混入した。
fs(ファイル操作)などNode.js組み込みモジュールはブラウザに存在しない。Client Component、またはそこからimportされるファイルで使うと、バンドル4にNode専用コードを入れようとして解決に失敗する。データベースクライアントやサーバー専用SDKをimportした場合も、その依存の奥で同じことが起きる - ファイル名の大文字小文字。macOSは
header.tsxとHeader.tsxを区別しないが、デプロイ先のLinuxは区別する。手元で動いてCIで落ちる場合の定番
直し方:
- パターン1:そのモジュールを使う処理をServer Component、Route Handler、Server Functionsなどサーバー側に移す。「どこから混入したか分からない」ときは、エラーに出るimportチェーンを上から辿る
- パターン2:importのパスと実ファイル名の大文字小文字を一致させる
防ぎ方:サーバー専用のファイルには import "server-only" を書いておくと、クライアントに混入した時点で明確なエラーで止めてくれる。逆の client-only もある。AI生成コードはこの宣言をまず書かないので、自分で足す価値がある。
出典: Next.js公式: Module not found / Next.js公式: 環境汚染の防止(server-only)
5. 環境変数がundefined/変えたのに反映されない
症状:.env に書いたはずの環境変数がブラウザ側で undefined。もしくは、本番の環境変数を変更したのにアプリが古い値を使い続ける。
何が起きているか:環境変数はサーバーのもので、ブラウザからは見えない。例外として NEXT_PUBLIC_ で始まる変数だけは、ビルド時に値が読み取られて、配信されるJSの中に文字列として焼き込まれる(インライン化)。
この仕組みから2つの帰結が出る。
- プレフィックスなしの変数(
API_SECRETなど)はクライアント側では読めない。undefinedになるのは仕様 NEXT_PUBLIC_変数はビルドした瞬間の値で凍結される。公式ドキュメントにも「ビルド後はこれらの環境変数の変更に反応しなくなる」と明記されている。本番サーバーで.envを書き換えても、再ビルドするまで何も変わらない
直し方:
- ブラウザで使いたい値には
NEXT_PUBLIC_を付ける。ただしソースコードに書いたのと同じで全世界に公開される。秘密鍵・APIシークレットには絶対に付けない - 秘密の値はサーバー側(Server Component・Route Handler)でだけ使い、結果だけをブラウザに返す
- 環境変数を変えたら再ビルド・再デプロイする。「変えたのに直らない」の大半はこれ
- 動的な参照(
process.env[name]のように変数名を変数で指定)はインライン化の対象外で常にundefinedになる。直接process.env.NEXT_PUBLIC_XXXと書く
防ぎ方:環境変数を追加するとき「これはブラウザに公開していい値か?」を毎回問う。AIは「動かすため」に平気でシークレットへ NEXT_PUBLIC_ を付けてくることがある。これはエラーが消える代わりに鍵が漏れるので、AI生成コードのレビューでは真っ先に見るポイント。
6. データが更新されない(キャッシュと静的化)
症状:DBやCMSを更新したのに、本番のページが古いデータを表示し続ける。開発中(npm run dev)は毎回最新になるので、本番に出して初めて気づく。
何が起きているか:主犯は静的プリレンダリング5。Next.jsは、リクエストごとに変わる情報(cookieや検索パラメータなど)を使っていないページを「静的にできる」と判断し、ビルド時に一度だけレンダリングしてHTMLを固定する。ページ内の fetch もビルド時の一度しか走らない。
キャッシュまわりはバージョンで既定が大きく変わった点に注意:
- Next.js 14まで:
fetchの結果もGET Route Handlerもデフォルトでキャッシュされた。ネット上の古い記事はこの前提で書かれている - Next.js 15以降(16も同様):
fetch・GET Route Handler・クライアント遷移はデフォルトでキャッシュされない方向に転換された。ただし上記の「ページ自体の静的化」は今もデフォルトで起きる
直し方(App Router / 15以降前提):
- 一定間隔で更新されればよい → ページまたはfetchに
revalidateを設定(例:export const revalidate = 60で60秒ごとに再生成) - 常に最新が必要 →
export const dynamic = 'force-dynamic'でページを毎リクエストレンダリングにする - 更新イベントを起点にしたい → 管理画面の保存処理などから
revalidatePath()/revalidateTag()を呼んで能動的に作り直す - 逆に「14から15に上げたら急に毎回fetchが走って遅くなった」場合は、キャッシュしてよいfetchに
cache: 'force-cache'を明示する
防ぎ方:ページを作るたびに「このページは①ビルド時に固まっていい ②何秒か古くていい ③常に最新」のどれかを決めて、対応する設定を書く。決めていないページが「意図せず①」になるのがこのエラーの正体。
出典: Next.js 15リリースノート(キャッシュ既定の変更) / Next.js公式: fetch / Next.js公式: route.js
7. blocked by CORS policy(外部APIが呼べない)
症状:ブラウザのコンソールに Access to fetch at 'https://api.other.com' from origin 'https://yoursite.com' has been blocked by CORS policy が赤く出る。
何が起きているか:ブラウザには「スクリプトから別オリジン6へのリクエストは、相手が明示的に許可した場合しか結果を読ませない」という安全装置(same-origin policy)があり、CORS7はその許可を伝える仕組み。外部APIが許可ヘッダーを返さなければブラウザが結果を遮断する。制限しているのはAPIでもNext.jsでもなくブラウザなので、こちらのコードをいくら直しても解決しない。
直し方:
- 自分のNext.jsに中継役のRoute Handler(例:
app/api/proxy/route.ts)を作り、ブラウザはそこへfetchする。ブラウザ→自サイトは同一オリジンなのでブロックされず、自サイト→外部APIはサーバー同士の通信なのでCORSの対象外 - 相手のAPIが自分の管理下(自作API等)なら、API側でCORSヘッダー(
Access-Control-Allow-Originなど)を返す - ついでの利点として、外部APIのキーを中継サーバー側に置けるので、エラー5で書いた「キーをブラウザに晒す」問題も同時に解決する
防ぎ方:外部APIを使う設計をするとき、最初から「ブラウザから直接呼ぶか、サーバー経由にするか」を決める。キーが要るAPIは無条件でサーバー経由。AIに書かせる場合は「外部APIはRoute Handler経由で呼んで」と指示する。
まとめ:1枚の表
| エラー | 根本原因 | まずやること |
|---|---|---|
| Hydration failed | サーバーとブラウザで描画結果が違う | 時刻・乱数・localStorageをuseEffectへ |
| window is not defined | ブラウザ専用APIをサーバーで実行 | useEffect内へ移す / dynamic + ssr:false |
| useState only works in... | "use client"の境界がない・関数を境界越しに渡した | 操作部分だけ切り出して"use client" |
| Module not found | Node専用モジュールの混入 / 大文字小文字 | サーバー側へ移す / server-only宣言 |
| 環境変数がundefined | NEXT_PUBLIC_はビルド時に焼き込み | 用途を分ける・変えたら再ビルド |
| データが更新されない | 静的プリレンダリング+キャッシュ | revalidate / force-dynamic を明示 |
| CORS | ブラウザの安全装置 | Route Handlerで中継 |
7つ並べたが、防ぎ方は結局ひとつに集約される。コードの各行について「これはサーバーで走るのか、ブラウザで走るのか」を答えられるようにする。これができれば、この記事のエラーはほぼ書く前に防げる。
正直な話
この記事の内容はNext.js 16.2系(2026年7月時点の安定版)を前提にしている。特にキャッシュの既定は14と15で逆転しているので、検索で出てくる古い記事やAIが学習した古い知識と食い違うことがある。エラーメッセージの文言もバージョンで変わる。困ったら一次情報(nextjs.org/docs/messages 配下の該当ページ)を見るのが最短。
それから、ここに載せたのは「よくある6種」であって全部ではない。App Routerのキャッシュだけでも本が一冊書けるくらいの奥行きがある。
それでも動かないとき
AIに生成させたNext.jsがこの手のエラーで止まっている場合、診断・修正のサービスをやっている。エラーメッセージとリポジトリ(または該当ファイル)があれば、どこで境界を踏んでいるかを特定して直す。
Footnotes
-
サーバーサイドレンダリング(SSR) — ページのHTMLをブラウザではなくサーバー側で組み立てて送る方式。表示が速く、検索エンジンにも読まれやすい。 ↩
-
ハイドレーション — サーバーから届いた「静的なHTML」に、ブラウザ側でイベントハンドラなどを取り付けて操作可能なReactアプリとして起動する処理。乾いたHTMLに水(動き)を与えるという比喩。 ↩
-
Server Component — サーバーでだけ実行されるReactコンポーネント。実行結果だけがブラウザに送られ、コンポーネント自体のJSは配信されないため、ページが軽くなる。 ↩
-
バンドル — ブラウザに配信するために、多数のソースファイルをまとめて変換したJSファイル群のこと。ここに入れられないもの(Node専用コード)を入れようとするとビルドが失敗する。 ↩
-
静的プリレンダリング — アクセスが来る前(ビルド時)にページのHTMLを作り置きしておくこと。配信は最速になるが、内容はビルド時点で固定される。 ↩
-
オリジン — 「スキーム+ドメイン+ポート」の組(例: https://example.com)。この3つが全部同じなら同一オリジン、どれかが違えば別オリジン。 ↩
-
CORS(オリジン間リソース共有) — 別オリジンからのアクセスを「この相手なら許可する」とサーバーがHTTPヘッダーで表明する仕組み。表明がなければブラウザは結果をスクリプトに渡さない。 ↩