PDFのロードのたびに、同じReferenceErrorでクラッシュした。スタックトレースは役に立たない場所を指しており、コードの指針となった仕様書は、紙の上では完璧に合理的に見えた。そこには、フィーチャーフラグを確認すること、そして各リージョンをpageWidthの半分と比較することが記されていた。何が起こるべきかは説明されていた。しかし、pageWidthがどこから来るべきかについては説明されておらず、そのたった一つの欠落が、パイプライン全体を崩壊させるのに十分だった。

アーキテクチャドキュメントは、振る舞いを説明することには長けている。しかし、境界(boundaries)を説明することに関しては、しばしばひどいものだ。「関数はxをpageWidthと比較する」という一文は、技術的な契約ではない。それは、平易な英語の中に依存関係を隠蔽したナラティブ(物語)に過ぎない。開発者がその一文を読み、pageWidthという名前で参照するモジュールレベルの関数を書いたとき、そのコードは記述を満たしているため、正しく見える。しかし、ランタイムがその名前を解決しようとすると、スコープ内に何も見つからず、例外がスローされる。

本来なら単純であるはずだったリファクタリング

ページ組み立てのリファクタリング中に、まさにこのパターンを目にした。仕様書には、一見するとクリーンな2つの要件が記載されていた。

  • FEATURE_LAYOUTを確認する。
  • 各リージョンをpageWidth / 2と比較する。

開発者は指示に忠実に従った。彼らはモジュールスコープにユーティリティ関数を抽出し、pageWidthを引数として定義することなく、関数本体に直接放り込んだ。仕様書には、pageWidthが引数リストを通じて渡されなければならないとは書かれていなかった。また、関数がモジュールスコープに存在し、そこではpageWidthがもはや見えない状態であるとも書かれていなかった。単に、実装者が実行コンテキストを暗黙的に理解していることを前提としていたのだ。

その結果、PDFをロードするたびにReferenceErrorが発生した。変数がモジュールスコープに存在しなかったため、関数は即座に例外をスローした。もし仕様書が関数の境界とその入力を明示的に指定していれば、開発者はpageWidthを渡していたはずであり、バグは構造的に発生し得なかっただろう。代わりに、その指示は罠のように機能し、実装者が存在しない親スコープへと手を伸ばすよう誘い込んでしまった。

「使用する(Uses)」という言葉の4つの意味

より深い問題は、散文(プローズ)には型システムが欠如していることだ。仕様書に「関数はXを使用する」と書かれているとき、現代のJavaScriptコードベースにおいて、その一文は少なくとも4つの特定の意味で曖昧である。

  • 関数はXを仮引数として受け取る。
  • 関数は同じファイル内で宣言されたモジュールレベルの変数からXを読み取る。
  • 関数はネストされた親スコープからXをクロージャとして取り込む。
  • 関数は、渡された大きなオブジェクトからXを抽出する。

これらの選択肢はどれも、仕様書の文言を満たしている。どれも静的解析をパスする。しかし、特定の境界に対して正しいのはそのうちの1つだけであり、誤った選択は、コンパイル時に静かに依存関係を境界を越えて漏洩させてしまう。

開発者は通常、記述する瞬間に最も抵抗の少ない道を選ぶ。もしpageWidthがたまたま外側のスコープに存在していれば、関数のシグネチャを変更するのではなく、そこから読み取るだろう。クロージャは依存関係を隠してしまう。コードは初回実行時に動作し、テストスイートをパスし、リリースされる。数週間後、誰かが再利用のため、あるいは可読性向上のために、その関数を別のファイルに移動させる。すると親スコープが消滅する。コードは壊れ、その原因は元の隠れた依存関係であるにもかかわらず、あたかも新しいリグレッションが発生したかのように見える。

Web Workersが証拠を消し去る

この問題は、アーキテクチャにWeb Workersが導入されると、真に厄介なものになる。ワーカー内でエラーが発生すると、ブラウザは最も必要とする情報を剥ぎ取ってしまう。

実際に起こることはこうだ。ワーカー内でキャッチされなかった例外が発生すると、ErrorEventが発行される。もしワーカーがそのエラーをメインスレッドに転送する場合、一般的なパターンはmessage文字列を取得して境界を越えてポストすることだ。メインスレッドはその文字列を受け取り、それをもとに新しいErrorオブジェクトを構築して、ログ出力するか再スローする。DevToolsに表示されるのは、メインスレッドのメッセージハンドラ内で再構築されたエラーである。元のファイル名、行番号、およびスタックトレースは破棄される。失敗の真の場所は不可視となる。

したがって、欠落したpageWidthがワーカー内でReferenceErrorを引き起こしたとき、メインスレッドはメッセージが処理された箇所で「pageWidth is not defined」というテキストのみを報告した。実際のモジュールレベルの関数は、