デバッグ

プログラムの不具合を発見し、原因を特定して修正する一連の作業

プログラミング開発手法

デバッグとは

デバッグ (Debug) は、プログラムの不具合 (バグ) を発見し、原因を特定して修正する作業である。書くこと自体より時間を取られやすい作業で、その理由は「症状が出た場所」と「原因がある場所」が離れるところにある。null 参照で落ちた行は、その値を null のまま返した関数でも、その関数に空のレスポンスを渡した通信処理でもない。デバッグの実体は修正ではなく、症状から原因までの距離を詰める探索である。

デバッグの基本手順

デバッグは以下の手順で進める。

  1. バグを再現する (同じ条件で同じエラーを起こす)
  2. 原因の範囲を絞り込む (どのファイル、どの関数か)
  3. 仮説を立てる (なぜこの動作になるのか)
  4. 仮説を検証する (ログ出力、ブレークポイント)
  5. 修正する
  6. 再現手順で修正を確認する

この手順で要になるのはステップ 1 の再現である。再現手順がなければ、修正したつもりの変更が本当に効いたのかを確認できず、たまたま症状が出なかっただけの状態を「直った」と誤認する。再現できたら、次に手順を削れるところまで削る。5 画面たどる手順が 1 リクエストに縮まれば、それだけで原因の範囲は劇的に狭まる。

どうしても再現しない場合は、再現を諦めるのではなく再現できる材料を集める側に回る。発生時刻前後のログ、入力値、環境差 (バージョン・タイムゾーン・データ量) を突き合わせ、足りない情報があればログや計測を先に足して次の発生を待つ。「再現しないので様子を見る」で閉じたバグは、条件が揃ったときに同じ形で戻ってくる。

console.log デバッグ

最も手軽な方法は console.log で変数の値を出力することである。

function calculateTotal(items: Item[]) {
  console.log("items:", items);           // 入力を確認
  const subtotal = items.reduce((sum, item) => {
    console.log("item.price:", item.price); // 各要素を確認
    return sum + item.price;
  }, 0);
  console.log("subtotal:", subtotal);     // 中間結果を確認
  return subtotal * 1.1;
}

手軽だが、大量に入れると出力が読みにくくなる。修正後に消し忘れるリスクもあるため、リンターで本番コードへの混入を弾く設定を入れておくとよい。

ブレークポイントより劣った手法というわけではなく、向く場面が違う。1 万回回るループの何回目で壊れるか、非同期処理がどの順で走ったか、実行を止めると再現しなくなるタイミング依存の不具合、デバッガーを接続できない本番寄りの環境。これらは実行を止めずに履歴が残る出力型が優位である。逆に、止めた時点の変数を隅々まで見たい場面や、どこで値が変わったのか見当もつかない場面はブレークポイントが速い。

ブレークポイント

IDE やブラウザの開発者ツールでは、コードの特定の行で実行を一時停止できる。停止中に変数の値を確認し、1 行ずつ実行を進められる。

操作意味
Step Over現在の行を実行し、次の行へ
Step Into関数の中に入る
Step Out現在の関数から抜ける
Continue次のブレークポイントまで実行

これが可能なのは、デバッガーが外からコードを読んでいるからではなく、処理系側がデバッグ用の窓口を持っているからである。Node.js の場合、--inspect を付けて起動すると既定で 127.0.0.1:9229 で待ち受け、プロセスごとに固有の UUID が割り当てられる。デバッガーはその接続先に Chrome DevTools Protocol のクライアントとしてつながり、停止・再開・変数の取得を要求する。IDE の画面はこのやり取りの見た目にすぎず、同じ窓口へ別のクライアントを繋いでもよい。

node --inspect app.js       # 起動してすぐ実行が始まる (接続前に走る)
node --inspect-brk app.js   # 接続後に 1 行目で停止する
node --inspect-wait app.js  # 接続されるまで実行しない

最初から追いたいのに --inspect を使うと、接続作業をしている間に問題の箇所を通過してしまう。起動処理を追う場合は --inspect-brk--inspect-wait を選ぶ。接続先の一覧は Chrome や Edge で chrome://inspect (edge://inspect) を開くと Remote Target として表示される。ブラウザを使わず node inspect script.js で同梱のコマンドラインデバッガーを使う手もあり、cont / next / step / out が上の表の操作に対応する。

止める場所を指定するだけでなく、条件を付けられることも押さえておきたい。i === 9998 のような条件付きブレークポイントを使えば、1 万回のループで手動の Continue を繰り返す必要はない。式を登録して停止ごとに評価させるウォッチ (コマンドラインデバッガーなら watch('式')、解除は unwatch('式')) は、値がいつ変わるかを追うときに効く。停止せずにメッセージだけ出すログポイントに対応した環境なら、コードを書き換えずに console.log 相当を差し込める。

二分探索デバッグ

バグの原因がわからないとき、コードの中間地点にログを入れて「ここまでは正常か」を確認する。正常なら後半に、異常なら前半にバグがある。この繰り返しで原因を絞り込む。

全体のコード
├── 前半 ← ここまで正常?
│   ├── 前半の前半
│   └── 前半の後半
└── 後半 ← ここで異常?
    ├── 後半の前半
    └── 後半の後半

同じ考え方はコードの範囲ではなく時間軸にも使える。「先週は動いていたのに今日は壊れている」という形のバグは、原因を読んで探すより、正常だった commit と壊れている commit の間を二分探索するほうが速い。git にはこれを行う git bisect があり、公式ドキュメントでも二分探索によって不具合を持ち込んだ commit を特定するコマンドと定義されている。1000 commit の差でも 10 回程度の判定で 1 つに絞れる。判定を自動化できるテストがあるなら git bisect run に任せられるため、テストを 1 本書く手間が探索全体より安くつくことが多い。

ラバーダックデバッグ

コードの動作を声に出して説明する手法である。ゴム製のアヒル (ラバーダック) に向かって説明するという逸話が名前の由来。人に説明しようとすると、自分の理解があいまいな部分に気づき、バグの原因が見えてくることがある。

よくあるバグのパターン

パターン対策
off-by-oneループが 1 回多い/少ない境界値をテスト
null 参照undefined のプロパティにアクセス省略可能連鎖 (?.) や既定値で受ける
型の不一致文字列の "1" + 1 が "11"型を明示的に変換
非同期の順序await の付け忘れasync/await を正しく使う
スコープの誤り意図しない変数を参照変数のスコープを狭く保つ

デバッグの心構え

心構え説明
思い込みを捨てる「ここは正しいはず」が原因であることが多い
1 つずつ変える複数箇所を同時に変えると原因がわからなくなる
休憩する行き詰まったら離れる。戻ると解決策が見えることがある
バージョン管理を活用動いていた状態との差分を確認する

最後に、原因が分かった時点で終わりにしないことが後の時間を決める。同じ間違いが他の箇所にも書かれていないかを確認し、再現手順をテストとして残す。テストを残さない修正は、次の改修で誰かが同じ条件を踏んだときに何も警告しない。修正の価値は症状を消したことではなく、その症状が二度と黙って戻ってこない状態を作ったところにある。

この記事は役に立ちましたか?

関連用語

関連する記事