2026-08-21 公開 / やらかした話
その自動処理は一度も成功していなかった。判定関数が「中身がある」を取り違えていた
定期的に動かしている処理の1つが、サーバーに置いてから一度も成功していなかったと分かりました。エラーは出ていません。ログには成功として記録されていました。原因は、処理の中で使っていた「結果に中身があるか」を確かめる判定が、見るべきでないものを見ていたことでした。
やっていること
処理の流れはよくある形です。何らかの入力から結果を組み立て、それが空でないことを確かめてから、使う場所へ書き出す。空のまま上書きすると前の内容が消えてしまうので、途中に確認を挟んでいます。
結果を組み立てる
↓
中身があるか確かめる ← ここが今回の問題
↓
中身があれば書き出す
結果は入れ子になったデータで、いちばん外側が配列になっています。その外側の配列に1つ以上要素があれば成功、というのが本来の判定でした。
判定が見ていたもの
実装していた判定は、雑に言えば「このデータのどこかに、要素の入った配列があるか」を見るものでした。外側だけを見るつもりで書いたのに、中を掘って探す作りになっていたのです。
問題は、この結果のデータが入れ子の中にも配列を持っていたことです。設定や定義の一覧のようなもので、これは結果が空でも常に入っています。
// 期待していた判定: 外側の配列に要素があるか
{ "items": [] } → 空。書き出さない
// 実際の判定: どこかに要素のある配列があるか
{ "items": [], "categories": ["a","b"] } → 「ある」と判定される
つまり結果がいつも空でも、判定はいつも真でした。そして「中身がある」ことになったので、そのまま書き出しに進み、正常終了します。
この壊れ方には、失敗の痕跡が一切残りません。例外も出ず、終了コードも正常で、ログにも成功と記録されます。動いていないことを示すものが、結果が空であること以外に何も無い状態です。そして結果を毎回目で見る運用にはしていませんでした。
なぜ長い間気づかなかったのか
いくつか重なっています。
- 手元では動いていた。開発中は入力を用意して試すので、結果が空になりません。判定が甘くても、正しい答えが出ます
- サーバーでは条件が違った。置いた環境では入力の取得がうまくいっておらず、結果が常に空でした。ここで初めて判定の甘さが表に出ます
- 成功として記録されていた。ログを見に行っても「成功」しか書いていません
つまり「甘い判定」と「入力の失敗」の2つが重なって初めて症状になる構造でした。片方だけなら表面化しません。手元で再現しようとしても、入力が取れてしまうので再現しないのです。
直し方: 見る場所を1つに固定する
直したのは、「どこかにあるか」ではなく「いちばん外側がどうか」だけを見るようにしたことです。
// 直す前: 中を掘って探す(入れ子の配列を拾ってしまう)
どこかに要素の入った配列があるか
// 直したあと: 外側だけを見る
いちばん外側が配列で、かつ要素が1つ以上あるか
同じ判定を別の言語でも書いていたので、そちらも同じ形に揃えました。同じ判断を2箇所に持つなら、少なくとも書き方は揃えておかないと、片方だけ直して終わります。
直したあとに足した確認
直しただけでは、また同じ種類の見落としが起きます。判定を正しくしても、「正しく判定した結果、いつも空だった」という事態は起こりうるからです。そこで確認を2つ足しました。
1. 空だったことを、成功とは別に記録する
これまでは「書き出した」か「エラー」の2状態でした。ここに「正常に動いたが、結果が空だった」を足しました。エラーではないので止める必要はありませんが、続くようなら異常です。2つの状態しか無いと、この中間が成功側に丸め込まれます。
2. 前回の結果と比べる
結果の件数を前回と比べ、ゼロが続いていないかを見るようにしました。1回ゼロなのは、たまたまその日は対象が無かっただけかもしれません。何日も続くゼロは、対象が無いのではなく取れていないことを示します。
この種のバグの見分け方
今回の経験から、同じ形のバグを疑う条件を言葉にしました。「ある/ない」を判定するコードで、判定対象が入れ子のデータのときです。
入れ子のデータは、たいてい「本体」と「本体以外」を両方含んでいます。件数、更新時刻、設定、分類。これらは本体が空でも入っています。「中身があるか」を漠然と聞くと、この本体以外を拾って真を返します。
だから判定を書くときは、「どこの」を必ず明示するようにしました。「中身があるか」ではなく「外側の配列に要素があるか」。日本語で言い直せない判定は、コードでも曖昧になっています。
持ち帰れること
- 「中身があるか」を漠然と聞かない。入れ子のデータは本体以外も含むので、どこを見るかを明示します
- 手元で再現しないバグは、環境の差が条件に入っている。「甘い判定」と「入力の失敗」のように、2つ重なって初めて症状になることがあります
- 成否の2状態では足りない。「正常に動いたが結果が空」を独立した状態として記録しないと、成功側に丸め込まれます
- 1回のゼロと、続くゼロは意味が違う。前回と比べる仕組みがあると、後者だけを拾えます
- 同じ判断を2箇所に持つなら、書き方を揃える。揃っていないと片方だけ直して終わります
- 判定を日本語で言い直せないなら、コードでも曖昧。「どこの」が言えるまで、書き直します
この件でいちばん怖かったのは、気づいたきっかけが不具合の症状ではなかったことです。まったく別の作業で周辺を触っていて、たまたま結果の中身を見て気づきました。あのとき見ていなければ、今も成功と記録され続けていたはずです。「エラーが出ていないから大丈夫」は、エラーを出す仕組みがあるときにしか使えないと考えるようになりました。