← 解説「原因の切り分け — 範囲を半分にしていく型」に戻る
サンプルコードで身につける: 原因の切り分け — 範囲を半分にしていく型
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1二分探索 — テンプレートを半分ずつ消す
ページを開くと落ちるがどの行かは不明、という場面ではテンプレートの半分をコメントアウトして試します。症状の有無で原因の側が分かるので、数回の繰り返しで1行まで絞り込めます。
<!-- バグ: このページを開くと ERROR TypeError で真っ白になる -->
<!-- 試行1: 後半をコメントアウト → エラー消えた → 原因は後半 -->
<section>
<h2>{{ user().name }}</h2>
<p>{{ user().email }}</p>
</section>
<!--
<section>
<h2>注文履歴</h2>
@for (o of orders(); track o.id) {
<p>{{ o.items[0].name }} ほか</p>
}
</section>
-->
<!-- 試行2: 後半の中をさらに半分に… を繰り返して1行に絞る -->
<!-- 犯人: o.items[0].name → items が空の注文があると落ちる -->
<!-- 直し方: 空配列に備える -->
<!-- @if (o.items.length > 0) { <p>{{ o.items[0].name }} ほか</p> } -->
2git diff — 動いていた状態との差分を見る
「さっきまで動いていた」バグの原因は、最後のコミット以降の変更の中にほぼ必ずあります。闇雲にコードを読むのではなく、まず差分の一覧に容疑者を限定するのが近道です。
# バグ: 昼までは動いていたのに、夕方になったら一覧が壊れた
# 1. 最後のコミットからの変更点を一覧する(容疑者リスト)
git diff
# 出力例(抜粋):
# --- a/src/app/user-list/user-list.ts
# +++ b/src/app/user-list/user-list.ts
# - users = computed(() => this.all().filter((u) => u.active));
# + users = computed(() => this.all().filter((u) => u.isActive));
# ^^^^^^^^
# リネームしたつもりが、データ側のプロパティ名は active のまま
# 2. どのファイルを触ったかだけ知りたいとき
git diff --stat
# 3. 「本当にこの変更が原因か」を確かめたいとき:
# 変更を一時退避して動いていた状態に戻す
git stash # → 動いた! やはり退避した変更の中に原因がある
git stash pop # 変更を戻して直しにかかる
3最小再現 — バグだけを小部屋に移す
本物の画面は要素が多すぎて原因が埋もれます。バグに関係しそうな部分だけを小さなコンポーネントに移して再現できれば、原因はその数行の中にあると確定できます。再現しなければ消した要素の中です。
// 本物: 500行のダッシュボードで「選択が勝手に解除される」バグ
// → 通信・ルーティング・他の部品を全部捨てて、疑いの核だけ残す
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-repro',
template:
'<button (click)="reload()">再読込</button>' +
'@for (t of tags(); track $index) {' +
' <label><input type="checkbox" /> {{ t }}</label>' +
'}',
})
export class Repro {
tags = signal(['red', 'blue']);
reload() {
// 数行でもバグが再現した! → 原因はこの範囲内と確定
// 犯人: track $index + 配列の作り直しで DOM が作り直され、
// チェック状態(DOMだけが持つ状態)が失われる
this.tags.set(['red', 'blue']);
}
}
// 直し方: track t のように値そのものを追跡キーにして
// 同じ項目のDOMを使い回させる(チェック状態が保たれる)
4データ→ロジック→表示の順に疑う
壊れる場所は大きく3層に分かれます。上流のデータから順に値を確認し、正しいと確かめられた層を容疑者から外していきます。この例ではロジック層のフィルタ条件に原因が見つかります。
// バグ: 「在庫あり」フィルタを付けると商品が1件も出ない
// [1] データ層: Network タブで /api/items の Response を見る
// [{ "id": 1, "name": "ペン", "stock": 12 }, ...]
// → データは正しい。データ層は容疑者から外す
// [2] ロジック層: ブレークポイントで加工後の値を見る
inStockItems = computed(() =>
this.items().filter((item) => item.stock > 100),
// ^^^
// 犯人発見: 「在庫あり(> 0)」のつもりが 100 と書いていた
// → 停止中に this.inStockItems() を評価すると [] で確定
);
// [3] 表示層: 今回はここまで来る前に原因が見つかった
// (もしロジックまで正しければ @for や @if を疑う)
// 直し方: item.stock > 0 に修正
//
// この「上流から順に確認して容疑者を外す」型を持っていると、
// どんな不具合でも調べる場所に迷わなくなる
5一度に一箇所 — 同時に変えると迷宮入りする
焦って複数箇所を同時に直すと、直っても壊れてもどの変更が効いたのか分からなくなります。仮説を1つ立てて1箇所だけ変えて確かめる、を繰り返すのが結局いちばん速い進め方です。
// バグ: 合計金額が表示されない
// ── 悪い進め方(同時に3箇所変更)──
// ・URLを '/api/cart' から '/api/carts' に変えた
// ・computed の計算式も書き換えた
// ・テンプレートの @if の条件も変えた
// → 直った! …が、どれが効いたのか分からない
// → 不要な2つの変更がコードに残り、次のバグの種になる
// ── 良い進め方(仮説→1箇所→確認)──
// 仮説1: URLが違う?
// → Network タブを見る → 200 で返っている → 仮説棄却。URLは戻す
// 仮説2: 計算式が違う?
// → ブレークポイントで total() を見る → 正しい値 → 仮説棄却
// 仮説3: @if の条件が違う?
// → @if (total() > 0) だが total() は 0 円もありうる
// → ここだけ @if (total() !== null) に変更 → 直った!
// → 効いた変更は1つだけ。原因の説明もでき、余計な変更も残らない
// 鉄則: 変更は一度に一箇所。効かなかった変更は必ず元に戻す