【ITニュース解説】TypeError: __exportAll is not a function — the deploy that only breaks after you merge
2026年09月14日に「Dev.to」が公開したITニュース「TypeError: __exportAll is not a function — the deploy that only breaks after you merge」について初心者にもわかりやすく解説しています。
ITニュース概要
本番で「TypeError: __exportAll is not a function」エラーでサイトが停止。プレビューはOKだがマージ後に発生した。原因は同じモジュールを静的・動的に重複インポートしたこと。余計な動的インポートを削除し解決した。CIでマージ結果を検証、予防策を導入。プレビュー成功が本番成功を保証しない教訓を得た。
ITニュース解説
ITシステムにおいて、ソフトウェアが本番環境で予期せぬエラーを起こすことは、開発者にとって大きな課題である。今回取り上げるのは、本番環境で突然サイト全体が利用できなくなる「TypeError: __exportAll is not a function」というエラーについてだ。このエラーは、開発者が普段書いているコードとは異なる場所で発生し、プレビュー環境では検出されなかった。
このエラーの症状は、Webサイトのあらゆるページにアクセスしても「500エラー」が返ってくることだった。サーバーの実行ログには「TypeError: __exportAll is not a function」というメッセージが表示され、プログラムが特定の関数「__exportAll」を呼び出そうとしたときに、それが存在しないか、関数として認識されていないことを意味していた。スタックトレースを見ても、自分のアプリケーションコードではなく、サーバーが生成したと思われるファイル内でエラーが発生しており、モジュールの読み込み段階で問題が起きていることが示されていた。そのため、サイト全体が利用不能になっていた。
「__exportAll」とは、複数のJavaScriptファイルをまとめて効率よく実行するためのツールである「バンドラー」が内部的に使用するヘルパー関数の一つだ。開発者が「import * as 名前空間 from './モジュール'」のように、あるモジュール全体を一つのオブジェクトとしてインポートしようとした場合、バンドラーはこの名前空間オブジェクトを生成するために「__exportAll」のようなヘルパー関数を自動的に作成する。この関数が存在するということは、プログラムのどこかでモジュール全体をオブジェクトとして扱うインポートが行われていることを示している。
このエラーの根本的な原因は、同じモジュールをプログラム内で「静的インポート」と「動的インポート」の両方で読み込もうとしたことだった。静的インポートはファイルの先頭などで実行開始時にモジュールを読み込む方法で、動的インポートは実行中に必要になったタイミングでモジュールを読み込む方法だ。本来、動的インポートは初期ロードの高速化に役立つが、今回のケースでは、すでに静的インポートで読み込まれているモジュールに対して、意味もなく動的インポートも行われていた。
このような冗長な動的インポートがあることで、バンドラーは、既に読み込み済みのモジュールであっても、動的インポートのために名前空間オブジェクトを改めて構築しようとする。このとき、VercelとNitroという環境では、バンドル済みのサーバーコードをさらにLambda関数向けに再バンドルする処理が行われる。この二度目のバンドル処理の際に、モジュールが複数の「チャンク」(バンドラーが生成するコードのまとまり)に分割される。もし「__exportAll」ヘルパー関数を定義しているチャンクと、それを呼び出そうとするコードが含まれるチャンクが、互いに依存し合うような「循環参照」の関係になってしまうと問題が発生する。具体的には、関数が定義されきる前に呼び出されてしまい、まだ未定義の状態である「undefined」を参照してしまうため、「__exportAll is not a function」というエラーが発生したのだ。
このエラーがプレビュー環境のデプロイで検出されなかったのは、プレビューデプロイが特定のブランチのコミットに基づいてビルドされ、モジュールの構成やチャンクの分割方法が、最終的な本番環境へのマージコミットとは異なっていたためだ。問題は、複数のブランチがマージされた結果として生まれる、本番環境向けの「マージコミット」のビルドで初めて顕在化した。開発者が安全だと確認したプレビュービルドは、実際に本番環境にデプロイされる成果物とは異なり、誰もテストしていない成果物が本番環境でエラーを引き起こしたことになる。
このエラーに対する解決策は非常にシンプルで、冗長な動的インポートを削除し、既に存在していた静的インポートのみを使用することだった。「import { buildClaimEmail, verifyClaimToken } from './purchase-claims.server'」のように、必要な機能を一度だけインポートすればよい。このような、モジュールが既に読み込まれているにもかかわらず、再び動的インポートを行ってしまうパターンは「非効率な動的インポート」と呼ばれ、メリットがないばかりか、今回のような問題を引き起こす可能性がある。
将来的な再発を防ぐため、開発チームは二段階のチェックメカニズムを導入した。一つ目は「ソーススキャン」だ。これは、ソースコードを解析し、同じモジュールが動的かつ静的に両方でインポートされている箇所を検出する。二つ目は「出力スキャン」だ。これは、ビルドされたサーバーサイドレンダリング出力のインポートグラフを解析し、「__exportAll」ヘルパー関数を定義またはインポートするチャンクが循環参照の一部になっている場合にビルドを失敗させる。単にヘルパー関数が存在するだけで失敗させてしまうと、健全なビルドでもこの関数は存在するため、すべてのビルドが失敗してしまう。重要なのは、ヘルパー関数の存在そのものではなく、「循環参照」の中にヘルパー関数が含まれているかどうかを検出することだった。
これらのチェックをCI/CDパイプラインに組み込む際も、問題がブランチの最新コミットではなくマージコミットで発生するため、特別な配慮が必要だった。GitHub Actionsでは、プルリクエストイベント時にマージがシミュレーションされた結果のコードをビルド・テストできる。この仕組みを確実に機能させるためには、GitHubのリポジトリ設定で「ジョブを必須ステータスチェックとしてマークする」ことと、「マージ前にブランチを最新の状態にする」ことを有効にすることが重要だ。後者の設定がなければ、古いマージベースでテストが成功しても、誰もテストしていないコードが本番にデプロイされる可能性がある。
このバグは特定の環境に起因するが、得られる教訓は二つ、より一般的だ。一つ目は、ビルドの出力がモジュールグラフ全体に依存するようなシステムでは、「緑色のプレビュー」が必ずしも「緑色のデプロイ」を意味しないということ。デプロイ時にコードのチャンク構成が再編成されるシステムでは、このリスクを常に考慮する必要がある。二つ目は、導入した安全対策(ガード)が本当に機能するかどうかは、意図的にそれを失敗させて確認する必要があるということだ。今回も、問題のある動的インポートを意図的に再導入して、ビルドが期待通りにエラーメッセージを出して失敗するかどうかを確認し、導入したチェックの有効性を確かめた。