top of page

Velo

実装

備忘録

コラム

その他

カテゴリー

例外より見つけにくい、モジュール評価時の NaN

  • 1 時間前
  • 読了時間: 3分

依存が古いときに 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 なのだから、届いていないことにならない。だから対処は診断ではなく、書き方のほうを変えるしかなかった。

関連記事

Stores/Orders が存在しなかった

WDE0025 は権限や綴りの問題に見える。実際には注文コレクションの ID が世代で変わっていた。作成日のフィールド名まで変わる。

 
 
 

コメント


bottom of page