Node.js 25 + Next.js 15 で発生する localStorage.getItem is not a function エラーの原因と対処法
はじめに
Next.js 15 のプロジェクトで npm run dev を実行したところ、以下のエラーが発生して開発サーバーが正常に動作しなくなりました。
⨯ [TypeError: localStorage.getItem is not a function] {
digest: '2892703879'
}
[TypeError: localStorage.getItem is not a function]
⨯ [TypeError: localStorage.getItem is not a function] { page: '/ja' }
(node:2405) Warning: `--localstorage-file` was provided without a valid path
コード上で localStorage を直接呼び出している箇所はなく、原因の特定に時間がかかりました。本記事では、このエラーの根本原因と対処法を解説します。
環境
- Node.js : v25.2.1
- Next.js : 15.3.8
- next-intl : 4.3.5
- OS : macOS (Darwin 25.2.0)
エラーの根本原因
このエラーは Node.js 25 で導入された Web Storage API と Next.js 15 の組み合わせで発生します。
Node.js における localStorage の変遷
Node.js は v22 から Web Storage API(localStorage / sessionStorage)の実装を実験的に進めてきました。
| バージョン | localStorage の挙動 |
|---|---|
| Node.js 21以前 | globalThis.localStorage は undefined |
| Node.js 22〜24 | --localstorage-file フラグ指定時のみ有効(実験的機能) |
| Node.js 25.0.0 | Web Storage API がデフォルトで有効化 |
Node.js 25 では globalThis.localStorage オブジェクトがデフォルトで存在するようになりました。ただし、--localstorage-file フラグで有効なファイルパスを指定しないと、localStorage オブジェクト自体は存在するものの、メソッド(getItem, setItem 等)が機能しないという中途半端な状態になります。
具体的には、Node.js の Issue #60303 で報告されているように、localStorage が「すべてのプロパティアクセスに undefined を返す空のプロキシオブジェクト」になってしまう問題がありました。
なぜ Next.js 15 で問題になるのか
多くのライブラリでは、サーバーサイドでの localStorage アクセスを以下のようなガードで防いでいます。
// 一般的なガードパターン
if (typeof localStorage !== 'undefined') {
const value = localStorage.getItem('key');
}
Node.js 24 以前では globalThis.localStorage が undefined なのでこのガードが正しく機能しましたが、Node.js 25 では localStorage オブジェクトが存在するためガードをすり抜けてしまいます 。そして getItem を呼び出すと、メソッドが undefined なので TypeError: localStorage.getItem is not a function が発生します。
Next.js 15 の内部コードや依存ライブラリの一部がこのパターンに該当しており、サーバーサイドレンダリング時にエラーが発生していました。
エラー発生の流れ:
1. Next.js 15 が SSR でページをレンダリング
2. 内部コード or 依存ライブラリが typeof localStorage !== 'undefined' をチェック
3. Node.js 25 では localStorage オブジェクトが存在 → ガード通過
4. localStorage.getItem() を呼び出し
5. getItem が undefined → TypeError 発生
対処法の選択肢
この問題には主に2つのアプローチがあります。
方法A: Node.js のバージョンを下げる
Node.js を v24 以下にダウングレードすれば、globalThis.localStorage が undefined に戻るため問題は解消します。ただし、これは一時的な回避策です。
方法B: Next.js 16 にアップグレード(推奨)
Next.js 16 ではこの問題が修正されています(vercel/next-learn#1129)。本プロジェクトではこちらの方法を採用しました。
Next.js 15 → 16 アップグレード手順
1. パッケージの更新
npm install next@latest next-intl@latest react@latest react-dom@latest \
eslint-config-next@latest @eslint/eslintrc@latest
更新されたバージョン:
| パッケージ | 更新前 | 更新後 |
|---|---|---|
| next | 15.3.8 | 16.1.6 |
| next-intl | 4.3.5 | 4.8.2 |
| react | 19.1.1 | 19.2.4 |
| react-dom | 19.1.1 | 19.2.4 |
| eslint-config-next | 15.3.3 | 16.1.6 |
2. middleware.ts → proxy.ts のリネーム
Next.js 16 の最大の破壊的変更の一つが、Middleware から Proxy へのリネームです。
mv src/middleware.ts src/proxy.ts
ファイルの中身は変更不要です。middleware.ts は非推奨として残されていますが、将来のバージョンで削除予定です。
// src/proxy.ts(内容は middleware.ts と同一)
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
export default createMiddleware({
...routing,
localePrefix: 'as-needed'
});
export const config = {
matcher: [
'/((?!_next|api|favicon.ico|models|config|.*\\..*|ort-.*|.*\\.wasm|.*\\.onnx).*)'
]
};
3. lint スクリプトの変更
Next.js 16 では next lint コマンドが削除されました。eslint を直接呼び出すように変更します。
{
"scripts": {
"lint": "eslint ."
}
}
4. ESLint 設定の更新
eslint-config-next@16 はネイティブの flat config をエクスポートするようになりました。@eslint/eslintrc の FlatCompat ラッパーが不要になります。
更新前:
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
ignores: ["src/lib/ocr/**", "public/models/**"],
},
{
rules: {
"@next/next/no-img-element": "off",
},
},
];
export default eslintConfig;
更新後:
import nextConfig from "eslint-config-next/core-web-vitals";
import nextTypeScriptConfig from "eslint-config-next/typescript";
const eslintConfig = [
...nextConfig,
...nextTypeScriptConfig,
{
ignores: ["src/lib/ocr/**", "public/models/**", "scripts/**"],
},
{
rules: {
"@next/next/no-img-element": "off",
},
},
];
export default eslintConfig;
5. next.config.ts
webpack のカスタム設定(WASM / ONNX 対応)はそのまま残します。Next.js 16 では Turbopack がデフォルトのバンドラーになりますが、webpack 設定は next build --webpack で引き続き使用可能です。本プロジェクトでは Turbopack で問題なくビルドできたため、変更不要でした。
6. 変更不要だった箇所
params/searchParams: 既にawaitを使った非同期アクセスに移行済み- API routes :
NextRequest.nextUrl.searchParamsを使用しており変更不要 - Client components :
useParams()/useSearchParams()フックは変更不要 - i18n 設定 (
routing.ts,request.ts): 変更不要
検証結果
| チェック項目 | 結果 |
|---|---|
npm run typecheck | ✅ パス |
npm run lint | ✅ パス(既存の warning 1件のみ) |
npm run build | ✅ Turbopack でビルド成功 |
npm run dev | ✅ localStorage エラー解消 |
まとめ
- 根本原因 : Node.js 25 で Web Storage API がデフォルト有効化され、
localStorageオブジェクトが存在するが--localstorage-file未設定時にメソッドが機能しない状態になった。Next.js 15 の内部コードがこの「壊れた localStorage」に対応できていなかった - 対処法 : Next.js 16 へのアップグレードで解消。Node.js のダウングレードでも回避可能
- アップグレード時の注意点 :
middleware.ts→proxy.tsのリネーム、next lintの廃止、ESLint 設定のネイティブ flat config 対応が必要
Node.js のバージョンアップに伴う Web API の追加は、サーバーサイドで動作するフレームワークに思わぬ影響を与えることがあります。特に typeof ガードに頼った実装は、新しい API の追加で壊れる可能性があることを認識しておく必要があります。