やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

謎バグ 犯人はまさかの奴

 くだらない所でハマって、ものすごく悔しい。

 JavaScriptでちょっとコードを試したい時、REPLやCodePenを使うことがあります。でも、モジュールまで考慮すると結局は以下のようにindex.htmlファイルを作成して確認することになります。

index.html

<script>
window.addEventListener('DOMContentLoaded', async(event)=>{
  // ここにテストしたいコードを書く
})
</script>

 そこに次のようなコードを書きました。

index.html

<script>
window.addEventListener('DOMContentLoaded', async(event)=>{
  const A = 0
  [1,2].map(v=>v+1) // Uncaught (in promise) TypeError: Cannot read property 'map' of undefined
})
</script>

 ここでエラーが発生しました。どこが問題か判りますか?

Uncaught (in promise) TypeError: Cannot read property 'map' of undefined

 和訳すると「キャッチされないプロミス(非同期処理)内で型例外が発生しました:未定義のプロパティ 'map' を読み取れません」。

 エラー文は二つに大別できます。

  • プロミスうんぬん
  • 'map'うんぬん

 プロミスうんぬんはwindow.addEvent..., async(event)関数内で例外発生した、という意味でしょう。これは問題ありません。

 未定義のプロパティ 'map'という部分がおかしいです。mapArrayのメンバmapとして定義されているはずです。

 あるいは[1,2]Arrayと判断されなかったのでしょうか? でもそれならSyntaxErrorになりそうです。そもそも[1,2]は構文的に正しく配列を意味するはずです。

 REPLでもCodePenでも再現しません。上記index.htmlファイルを作成しブラウザで実行した時だけ再現します。

 何が問題か。ざっくり方針として思いついたのは以下三つです。誰が悪いのか。ブラウザか、仕様か、私か。

  • 私の環境のバージョンが低すぎる?(バグ?)
  • strictモードが関係している?(仕様?)
  • 私が何か大切なことを失念している?(私?)

 エラーが起きた時、悪いのは大抵自分です。これは経験則で知っています。でも具体的にどうすればいいか判りません。

 なぜこんなエラーが起きたか、原因が判りますか?

解答

 const A = 0の直後にセミコロンを付与するとエラーが消えました。

index.html

<script>
window.addEventListener('DOMContentLoaded', async(event)=>{
  const A = 0; // 文末にセミコロンをつけたら
  [1,2].map(v=>v+1) // エラーが消えた!
})
</script>

 まさか、と思って試したらセミコロンでした。

 つまり、おそらく構文解析でエラーになったのでしょう。

エラー文言はアテにならない

 でも構文解析の問題ならSyntaxErrorにすべきでは?

 実際のエラーはTypeErrorな上に、文言が意味不明でした。「未定義のプロパティ'map'が読み取れない」という文言ではなく「SyntaxError: セミコロンがありません」と書いてくれないと対処できません。

 ふと、甘酸っぱい思い出が蘇りました。古の時代、C言語でプログラミングを書いている時も、エラー文言が意味不明だとよく嘆いていたものです。私たちはエラー文言に期待してはならない。奴らは怪文書で人を惑わすマジシャンです。脳までバグらせるべく自然言語を吐き散らかす狂言者です。

 やはりコンパイラのエラー文言に人間味を期待するのは間違っている。

 信じるべきは状況証拠による自分の推論です。ひたすら検証し、事実確認を重ねて推論しましょう。

検証

 原因がセミコロンだと仮定すると。やはり構文解析に失敗したのでしょう。

 試しに内部コードを以下に変えてると、エラーが再現しました。

  const A = 0[1,2].map(v=>v+1) // エラーが再現した!
Uncaught (in promise) TypeError: Cannot read property 'map' of undefined

 セミコロンがないためconst A = 0[1,2].map(v=>v+1)の二行に渡って書かれた文が文字列結合した上で構文解析したのでしょう。(もっとも書いた人は改行をもって二文を区切ったつもりになっているため、このバグに気づきません。ここが落とし穴です)

 つまり0[1,2]となり、配列Arrayとして解釈されず、結果としてArrayが持っていたmapも参照できなかったことが、先述のエラー文言につながったと推測できます。

 ここで疑問なのは0[1,2]などというオブジェクトなりリテラルなりを定義することはできるのか? ということです。0[1,2]が定義もされないまま、それが持つプロパティmapを参照しようとすることなど不可能なはずですから。

 そもそもユーザ定義できる値の名前は先頭がアルファベットかアンダーバーのみだったはずです。試してみましょう。(セミコロンつけるつける絶対つける)

const 0[1,2] = 'x';
Uncaught SyntaxError: Unexpected number

 やはり名前の先頭が数字だと定義できません。これはSyntaxErrorになりますね。

 なぜこれは構文エラーなのにconst A = 0[1,2].map(v=>v+1)は構文エラーにならないのか。疑問ですが、これはもうJavaScript構文解析の話です。納得できませんが、これ以上の深追いは主題から外れるので一旦忘れます。あーモヤモヤする。

 次はリテラル値として解釈できると想定して試してみましょう。(セミコロンつけるつける絶対つける)

console.log(0[1,2]);
undefined

 なんと、undefinedを出力しました。0[1,2]はエラーではなく、正常終了した上でundefinedを返したのです。

 (これは直感に反します。先述の命名規則により、0[1,2]なる名前のものは存在できないはずだと考えるからです。もちろん予約語にも存在しません。ならば構文エラーとなるはずです)

 これはどう解釈すればいいのか。おそらく定義できなかった場合は必ずundefinedを返すのでしょう。そしてundefinedプリミティブ値であり、メソッドを持ちません。

 つまり0[1,2].map(v=>v+1)は、undefined.map(v=>v+1)を意味します。undefinedはメソッドを持たないため、「未定義のプロパティ'map'が読み取れない」という文言になったのでしょう。

 以下コードでエラー再現するか試してみましょう。(セミコロンつけるつける絶対つける)

undefined.map(v=>v+1);
Uncaught (in promise) TypeError: Cannot read property 'map' of undefined

 はい、エラー再現しました。推測は正しそうです。

エラーまでの経緯まとめ

 最初のコードは次でした。

const A = 0
[1,2].map(v=>v+1)
TypeError: Cannot read property 'map' of undefined
未定義のプロパティ'map'が読み取れない

 エラーの原因はconst A = 0の文末にセミコロンがないせいです。次の行にあるコードと結合してから構文解析されてしまうようです。その証拠に、次のコードは先述のエラーを再現します。

const A = 0[1,2].map(v=>v+1);

 0[1,2]mapプロパティを持ちません。それがエラー文言の正体です。そもそも0[1,2]自体がundefinedを返します。それは次のコードで証明できます。

console.log(0[1,2]);
undefined

 undefinedプリミティブ値のためメソッドを持ちません。当然mapプロパティも持ちません。よって以下コードで先述のエラーが再現します。

undefined.map(v=>v+1);

 以上の経緯により、今回のバグが起きました。

今後どうするか

 適時適切にセミコロンを使ったり使わなかったりする。(毎回悩み苦しむ)

 楽な解決策はありません。

 常にセミコロンをつける? たしかにバグ回避にはそれしかありません。ですが最適解とは限りません。なぜならパフォーマンス低下や新バグ作り込みリスクなど影響範囲が大きいからです。そこで対処方法を優先順に並べてみました。

  1. セミコロンつけない
  2. セミコロン必要な場所のみ付与する
  3. 全コード一律セミコロン付与する

 私としては1から2までの間で対応したいです。それはそれで今回のようにハマって苛つくのですが、だからといって全コード一律セミコロン付与は苦行どころの騒ぎではありません。ミスタイプによってバグを作り込みかねません。

 厄介なことにJavaScriptは文字数が実行パフォーマンスに直結します。わざわざMinifyするくらいですからね。よってセミコロンはできるだけ付与したくありません。

 まさかパフォーマンスまで考慮せねばならないとは。あまりに影響範囲が大きすぎます。エラー文言が判りにくい程度ならまだマシでした。UX、DXともに台無しにしするこのセミコロン問題。とても地味なわりに、意外なほど影響度が大きいです。

 じつに厄介ですね、JavaScriptセミコロンは。

ググってみる

 ハマっている人の多さよ。セミコロン問題は少し前に流行ったNULL安全なんかより致命的では? それを言い出すと他のアレコレ込みでJavaScriptはクソ言語であるという結論に行き着きそうです。

 肝心の対応は以下二点です。

  • ESLint(静的コードチェック)
  • セミコロンつけるつける絶対つける

 ようするにJavaScriptとかいうクソ言語の欠陥を人力で補いつつ、さらにわざわざ人間様がそれを補うLintツールを作って運用することで、なんとかカバーしているのが現状のようです。

 厄介なのはセミコロンを自動付与するツールを使うと挙動が代わってバグになりかねない所です。つまり、人間様が仕様を把握し、文末にセミコロンをつけねばなりません。

 アホかな? 楽をさせてはくれないようです。元はと言えばセミコロン付けるの面倒じゃね? という横着したい願望からこの仕様になったと思われるので、何とも責めにくい所です。もっとも、不完全になるくらいなら簡易さより完全さを優先して欲しいですが。

JavaScriptが悪い

 JavaScriptは次の問題を抱えています。

  1. セミコロンを忘れてもエラーを出してくれない(構文的にOK)
  2. なのにセミコロンがないとエラーになる場合がある(バカなの?)
  3. しかもエラー文言から原因がセミコロンであると特定できない(嫌がらせか?)

 「〜場合がある」というのが問題です。セミコロンは場合によって自動付与されるし、場合によって自動付与されない。よって場合によってエラーになったり、ならなかったりする。

 気分屋の面倒くさい彼女みたいな奴です。とりあえずスイーツをあてがうような感覚でセミコロンをつけていればいいだろ、と思いたくなるのも頷けますね。それくらい面倒くさいです。

俺ら<「必要だったら最初から構文エラーにしろよ」

彼女<「セミコロン忘れることくらいあるわよ。私が付けてあげる。嬉しいでしょ?」

俺ら<「確かに楽だし字数も減らせて嬉しいけど、お前エラー吐くじゃん」

彼女<「なによ、いつもは助けてるんだし、たまにエラー吐くくらい許してよ!」

俺ら<「いやいや、場合によって変わるって所が悪質なんだ。統一しろよ」

彼女<「はぁ?! せっかく私の優しさで助けてあげたのに!もう知らないっ!」

 うぜぇ……。

 善意であるはずの機能によって、逆に余計な手間をかけさせられて苛つくこと、ありますよね(エクセルのイルカとか)。悲しいすれ違いですが、結局はこちらが彼女をより深く理解し、開発してあげるしかありません。あるいは距離を置いて関わるのを避けるのも手です。こうなると宗教の話です。

宗教

 セミコロン。私はお前に消えて欲しい。だからできるだけ使わない。

 JavaScriptセミコロン問題。これは言語の構文解析による手法の問題です。しかしなぜかこのクソ言語、未だに使われ続けています。一時期は別の言語を作ると言っていたのにポシャったようですし。ならばもう、私たちはカルト教を立ち上げて推し活し、さも素晴らしい言語であるかのように自分自身の心を騙しつつ使い倒すしかありません。使いこなしている俺かっこいい的なノリで。さりとて事実誤認すれば思い通りのコードが書けないため、矛盾を孕んだ精神状態でコーディングすることを余儀なくされます。これは人生の縮図です。

 人生は自己矛盾です。人生は生きることが命題ですが、死ぬリスクを犯して狩りをせねば生きられません。矛盾を孕んでいます。生死は表裏一体です。生きたければ死に近づけ、となるのです。本末転倒です。プログラミングも同じです。

 プログラミングとは問題解決が命題です。問題を解決せねばならないのに、そのために新たな問題を抱えねばならないのです。問題解決する方法としてプログラミングをするけど、プログラミングにおける問題が次々と発覚し、その解決に追われて、本題の解決が遠のく。本末転倒ですね。

 一体俺は何をやっているんだ(哲学)

 安易に「セミコロンをつければいい」とか「Lintツールを使う」ことが解決策にならないのは一目瞭然です。それぞれには相応のコストがあります。特にLintツールは良さげに思えますが、罠です。元々はJavaScriptの問題なので、最終的にどこがどう問題なのか原因追求するのに異なる文脈を学習せねばならなくなり、余計な苦労をする羽目になります。ようするにJavaScript構文とLintルールと、その実装を理解していないと問題解決に至らないケースが出てくる可能性があります。

 糖衣構文・ライブラリ・フレームワーク等も同じ問題を抱えています。

 ツールを使うコストと、使わないコスト、どちらが低く済むか。ケースバイケースです。判断できません。

 便利に使える範疇はどこか。それを知るためには結局勉強せねばなりません。自分のやりたいことをするためには、結局、一番面倒な手法を取らざるを得ない。そんなことが往々にしてあるのです。

 「これは便利だよ!」と称するものたちを使うことで、逆に私たちが使われている。スマホユーザには耳の痛い話でしょう。はたしてどういう距離感で関わるか。話はそこに集約されます。SNS疲れ、推し活疲れ。大抵、人から勧められたり流行に乗っかっただけの受動的な活動はこの末路をたどります。人と関わることでハラスメントが起きるように、最初から関わらなければ問題は起きません。それでも関わらざるを得ないなら、宗教でごまかすしかありません。

 決して折れない信念。常軌を逸した自分だけのオリジナル宗教。これがなければとても付き合いきれません。

 セミコロン。私はお前に消えて欲しい。だからできるだけ使わない。消えろ消えろ消えろ。必要な時だけ使ってやる。一行で書きたい時もあるからな。それができないPythonのほうがクソだ。とにかく俺がセミコロンを使わされるんんじゃない。俺がセミコロンを使うんだ。