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.localStorageundefined
Node.js 22〜24--localstorage-file フラグ指定時のみ有効(実験的機能)
Node.js 25.0.0Web 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.localStorageundefined なのでこのガードが正しく機能しましたが、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.localStorageundefined に戻るため問題は解消します。ただし、これは一時的な回避策です。

方法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

更新されたバージョン:

パッケージ更新前更新後
next15.3.816.1.6
next-intl4.3.54.8.2
react19.1.119.2.4
react-dom19.1.119.2.4
eslint-config-next15.3.316.1.6

2. middleware.tsproxy.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/eslintrcFlatCompat ラッパーが不要になります。

更新前:

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.tsproxy.ts のリネーム、next lint の廃止、ESLint 設定のネイティブ flat config 対応が必要

Node.js のバージョンアップに伴う Web API の追加は、サーバーサイドで動作するフレームワークに思わぬ影響を与えることがあります。特に typeof ガードに頼った実装は、新しい API の追加で壊れる可能性があることを認識しておく必要があります。

参考リンク