The note to squeeze Wix
例外より見つけにくい、モジュール評価時の NaN
依存が古いときに export が消える話を追っていて、その途中で、もっと静かな壊れ方をする箇所を見つけた。日付ユーティリティの先頭にあった1行である。
const JST_OFFSET_MS = JST_OFFSET_MINUTES * 60 * 1000;JST_OFFSET_MINUTES は別ファイルから import した定数である。サイト上のそのファイルが古くて定数が届かないと、この行は undefined 60 1000 を計算する。
結果は NaN になる。例外は出ない。モジュールの読み込みは成功する。export も全部そろっている。
投げてくれたほうがましだった
関数が消える障害は、症状がはっきりしている。is not a function が出て、機能が止まる。困るけれど、止まったことは分かる。
NaN は止まらない。この値は日付の計算に使われていて、そこから作った時刻はすべて無効な日付になる。無効な日付との比較は、大なりでも小なりでも false を返す。つまり締切の判定は、書き方によって一方向に倒れる。キャンセルできるはずの日ができなくなるか、締切を過ぎた日ができてしまうかのどちらかで、どちらもエラーとしては現れない。
注文の締切に当てはめると、締切前なのにキャンセルのボタンが出ないか、締切を過ぎた注文が取り消せてしまうかのどちらかになる。前者は問い合わせが来るので、いつかは気付く。後者は気付かないまま、当日の集計だけが合わなくなる。間違った日付は、例外よりたちが悪い。
呼ばれた時点で確かめて、名指しで投げる
直し方は、評価時の計算をやめて関数にすることに尽きる。ただし、ただ関数へ移すだけでは NaN が返る場所が変わるだけなので、届いていないことをそこで検出する。
function jstOffsetMs() {
const minutes = Number(JST_OFFSET_MINUTES);
if (!Number.isFinite(minutes)) {
throw new Error(
'public/constants.js の JST_OFFSET_MINUTES が届いていません' +
'(サイト上の public/constants.js が古い可能性があります)。'
);
}
return minutes * 60 * 1000;
}Number.isFinite() を使うのは、undefined も NaN も文字列の混入も同じ判定で落とせるからである。文面には、どのファイルが古い可能性があるかを書く。この処理を呼ぶ側は日付の計算をしているだけで、依存の反映状況など知らない。
締切の日数と時刻も同じ扱いにした。こちらは undefined のプロパティを読むので TypeError にはなるが、そのときの症状は「キャンセルボタンが出ない」だけで、やはり原因は見えない。届いていないことを検出して名前を出す点は変わらない。
評価時の計算は、失敗の形で分類できる
モジュールの評価時に import した値へ触る書き方は、失敗したときの見え方で3つに分かれる。
書き方 | 依存が古いとどうなるか | 見え方 |
計算キー { [IMPORTED.KEY]: v } | TypeError で評価が失敗する | そのファイルの export が全滅する |
トップレベルの elevate(fn) | 渡した値が undefined で失敗する | 同上 |
定数を使った即時計算 | NaN が残る | 何も起きない。値だけが間違う |
上の2つは、依存の欠落を列挙する仕組みを入れれば診断できる。3つ目はその仕組みにも引っかからない。typeof で見れば number なのだから、届いていないことにならない。だから対処は診断ではなく、書き方のほうを変えるしかなかった。