はじめに

JavaScriptを書いていると、思ったように動かない、エラーメッセージが表示される、といった問題に必ず遭遇します。これらの問題をバグと呼び、バグを見つけて修正する作業をデバッグと呼びます。

デバッグはプログラミングにおいて避けては通れないスキルです。しかし、適切な手法を知っていれば、効率よくバグを特定し、修正できるようになります。

本記事では、以下の内容を初心者向けに解説します。

  • JavaScriptで発生するバグの種類
  • console.logを使った基本的なデバッグ手法
  • ブラウザDevToolsのデバッガ機能の活用
  • よくあるバグとその原因特定方法
  • バグの再現手順を作成するコツ
  • バグを未然に防ぐ予防策

バグの種類を理解する

JavaScriptで発生するバグは、大きく2つに分類されます。

flowchart TB
    A[バグの種類] --> B[構文エラー]
    A --> C[論理エラー]
    B --> D[スペルミス]
    B --> E[括弧の閉じ忘れ]
    B --> F[セミコロンの欠落]
    C --> G[条件式の誤り]
    C --> H[変数の型の不一致]
    C --> I[処理順序の問題]

構文エラー(Syntax Error)

構文エラーは、JavaScriptの文法ルールに違反した場合に発生します。コードの実行前または実行時に即座にエラーメッセージが表示されるため、比較的発見しやすいバグです。

1
2
3
4
5
6
7
// 構文エラーの例:閉じ括弧の欠落
function greet(name {  // SyntaxError: Unexpected token '{'
  return "Hello, " + name;
}

// 構文エラーの例:スペルミス
cosole.log("Hello");  // ReferenceError: cosole is not defined

論理エラー(Logic Error)

論理エラーは、文法的には正しいが、意図した動作と異なる結果になるバグです。エラーメッセージが表示されないため、発見が難しく、デバッグに時間がかかることがあります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 論理エラーの例:条件式の誤り
function isAdult(age) {
  // 本来は >= を使うべきところで > を使ってしまった
  if (age > 20) {
    return true;
  }
  return false;
}

console.log(isAdult(20));  // false(本来はtrueであるべき)

console.logを使ったデバッグの基本

console.log()は、最もシンプルで強力なデバッグツールです。変数の値や処理の流れを確認するために使用します。

基本的な使い方

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const userName = "田中";
const userAge = 25;

// 変数の値を確認
console.log(userName);       // "田中"
console.log(userAge);        // 25

// ラベルを付けて確認(推奨)
console.log("userName:", userName);  // userName: 田中
console.log("userAge:", userAge);    // userAge: 25

consoleオブジェクトの便利なメソッド

consoleオブジェクトには、log()以外にも便利なメソッドがあります。

メソッド 用途 使用例
console.log() 一般的なログ出力 console.log("処理完了")
console.error() エラー情報の出力 console.error("エラー発生")
console.warn() 警告情報の出力 console.warn("非推奨です")
console.table() オブジェクト・配列を表形式で出力 console.table(users)
console.dir() オブジェクトの詳細を展開表示 console.dir(element)
console.time() / console.timeEnd() 処理時間の計測 後述
console.trace() コールスタック(呼び出し履歴)を表示 console.trace()

オブジェクトと配列のデバッグ

オブジェクトや配列をデバッグする際は、console.table()を使うと見やすく表示されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const users = [
  { id: 1, name: "田中", age: 25 },
  { id: 2, name: "佐藤", age: 30 },
  { id: 3, name: "鈴木", age: 22 }
];

// console.logの場合
console.log(users);

// console.tableの場合(表形式で見やすい)
console.table(users);

ブラウザのコンソールでconsole.table()を使うと、以下のような表形式で表示されます。

(index) id name age
0 1 田中 25
1 2 佐藤 30
2 3 鈴木 22

処理時間の計測

パフォーマンスの問題を調査する際は、console.time()console.timeEnd()で処理時間を計測できます。

1
2
3
4
5
6
7
8
9
console.time("ループ処理");

let sum = 0;
for (let i = 0; i < 1000000; i++) {
  sum += i;
}

console.timeEnd("ループ処理");
// ループ処理: 5.123ms のように表示

条件付きログ出力

特定の条件のときだけログを出力したい場合は、console.assert()が便利です。

1
2
3
4
5
const age = 15;

// 条件がfalseの場合のみ出力
console.assert(age >= 18, "未成年です:", age);
// Assertion failed: 未成年です: 15

ブラウザDevToolsのデバッガ活用

console.log()だけでなく、ブラウザの開発者ツール(DevTools)に搭載されているデバッガを使うと、より効率的にバグを特定できます。

DevToolsの開き方

ブラウザ Windows / Linux Mac
Chrome F12 または Ctrl + Shift + I Cmd + Option + I
Firefox F12 または Ctrl + Shift + I Cmd + Option + I
Edge F12 または Ctrl + Shift + I Cmd + Option + I

ブレークポイントの設定

ブレークポイントを設定すると、コードの実行を特定の行で一時停止し、その時点での変数の値を確認できます。

flowchart LR
    A[コード実行開始] --> B[ブレークポイントで停止]
    B --> C{変数確認}
    C --> D[ステップ実行]
    D --> E[次のブレークポイントまで実行]
    E --> F[実行完了]

DevToolsの「Sources」タブで、行番号をクリックするとブレークポイントを設定できます。

debugger文の活用

コード内にdebugger文を書くと、DevToolsが開いている場合にその行で自動的に実行が停止します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function calculateTotal(items) {
  let total = 0;
  
  for (const item of items) {
    debugger;  // ここで実行が停止
    total += item.price * item.quantity;
  }
  
  return total;
}

const cart = [
  { name: "りんご", price: 100, quantity: 3 },
  { name: "バナナ", price: 80, quantity: 5 }
];

console.log(calculateTotal(cart));

ステップ実行の種類

ブレークポイントで停止した後、以下の操作でコードを1行ずつ実行できます。

操作 説明 ショートカット
Continue 次のブレークポイントまで実行を再開 F8
Step Over 現在の行を実行し、次の行で停止 F10
Step Into 関数の中に入って停止 F11
Step Out 現在の関数から抜けて停止 Shift + F11

Watchによる変数監視

Watchパネルに変数名や式を追加すると、ステップ実行のたびにその値を監視できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function findUser(users, targetId) {
  debugger;
  for (let i = 0; i < users.length; i++) {
    const user = users[i];
    if (user.id === targetId) {
      return user;
    }
  }
  return null;
}

Watchパネルに以下を追加して監視します。

  • i - ループカウンタの値
  • user - 現在処理中のユーザー
  • user.id === targetId - 条件式の結果

よくあるバグと原因特定方法

初心者がよく遭遇するバグと、その原因特定方法を紹介します。

undefined関連のエラー

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// エラー例
const user = { name: "田中" };
console.log(user.email.length);  // TypeError: Cannot read properties of undefined

// 原因特定
console.log("user:", user);           // { name: "田中" }
console.log("user.email:", user.email);  // undefined(emailプロパティが存在しない)

// 解決策:オプショナルチェーンを使用
console.log(user.email?.length);  // undefined(エラーにならない)

型の不一致によるバグ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// バグの例
const price = "100";
const quantity = 3;
const total = price * quantity;
console.log(total);  // 300(たまたま動く)

const newPrice = price + 50;
console.log(newPrice);  // "10050"(文字列連結になってしまう)

// 原因特定
console.log("price:", price, "型:", typeof price);      // price: 100 型: string
console.log("quantity:", quantity, "型:", typeof quantity);  // quantity: 3 型: number

// 解決策:明示的に型変換
const numericPrice = Number(price);
const correctNewPrice = numericPrice + 50;
console.log(correctNewPrice);  // 150

配列のインデックス範囲外アクセス

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// バグの例
const fruits = ["りんご", "バナナ", "みかん"];
console.log(fruits[3]);  // undefined(エラーにはならないが意図しない動作)

// 原因特定
console.log("配列の長さ:", fruits.length);  // 3
console.log("アクセス可能なインデックス: 0 ~", fruits.length - 1);  // 0 ~ 2

// 解決策:範囲チェック
function getFruit(index) {
  if (index < 0 || index >= fruits.length) {
    console.error(`インデックス ${index} は範囲外です`);
    return null;
  }
  return fruits[index];
}

非同期処理のタイミング問題

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// バグの例
let userData = null;

fetch("/api/user")
  .then(response => response.json())
  .then(data => {
    userData = data;
  });

console.log(userData);  // null(fetchが完了する前に実行される)

// 原因特定
console.log("同期処理: fetchの直後");
fetch("/api/user")
  .then(response => response.json())
  .then(data => {
    console.log("非同期処理: データ取得完了", data);
    userData = data;
  });

// 解決策:async/awaitを使用
async function loadUser() {
  const response = await fetch("/api/user");
  const userData = await response.json();
  console.log(userData);  // 正しくデータが表示される
}

スコープに関するバグ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// バグの例:varのスコープ問題
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);  // 3, 3, 3(すべて3が出力される)
  }, 100);
}

// 原因特定
console.log("ループ終了後のi:", i);  // 3(varはグローバルスコープ)

// 解決策:letを使用
for (let j = 0; j < 3; j++) {
  setTimeout(() => {
    console.log(j);  // 0, 1, 2(期待通りの出力)
  }, 100);
}

thisの参照問題

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// バグの例
const counter = {
  count: 0,
  increment: function() {
    setTimeout(function() {
      this.count++;  // thisがcounterを指さない
      console.log(this.count);  // NaN
    }, 100);
  }
};

counter.increment();

// 原因特定
const counter2 = {
  count: 0,
  increment: function() {
    console.log("メソッド内のthis:", this);  // counter2オブジェクト
    setTimeout(function() {
      console.log("setTimeout内のthis:", this);  // Window(グローバルオブジェクト)
    }, 100);
  }
};

// 解決策:アロー関数を使用
const counter3 = {
  count: 0,
  increment: function() {
    setTimeout(() => {
      this.count++;  // アロー関数は外側のthisを継承
      console.log(this.count);  // 1
    }, 100);
  }
};

バグの再現手順を作成するコツ

バグを修正するには、まずそのバグを確実に再現できる状態にすることが重要です。

再現手順の基本構成

flowchart TB
    A[環境情報の記録] --> B[最小限の再現コード]
    B --> C[入力データの特定]
    C --> D[期待する動作の明記]
    D --> E[実際の動作の記録]

再現手順のテンプレート

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
## バグ報告

### 環境
- ブラウザ: Chrome 120.0.6099.130
- OS: Windows 11
- Node.js: v20.10.0(該当する場合)

### 再現手順
1. フォームに「テスト」と入力する
2. 送信ボタンをクリックする
3. エラーメッセージを確認する

### 入力データ
~~~javascript
const input = "テスト";
\`\`\`

### 期待する動作
「送信完了」というメッセージが表示される

### 実際の動作
「TypeError: Cannot read properties of undefined」というエラーが発生

### コンソール出力

TypeError: Cannot read properties of undefined (reading ’length’) at validateInput (script.js:15:23) at handleSubmit (script.js:42:5) ```


### 最小再現コードの作成

バグを報告したり、助けを求めたりする際は、問題を再現できる最小限のコードを作成します。

~~~javascript
// 問題のあるコード(大規模なプロジェクトの一部)
// → 最小再現コードに簡略化

// 最小再現コード
const data = { items: null };

function getFirstItem(data) {
  return data.items[0];  // ここでエラー
}

getFirstItem(data);
// TypeError: Cannot read properties of null (reading '0')

バグを未然に防ぐ予防策

デバッグの最良の方法は、そもそもバグを作らないことです。以下の予防策を実践しましょう。

厳格モードの使用

"use strict"を宣言することで、潜在的な問題をエラーとして検出できます。

1
2
3
4
5
6
"use strict";

// 未宣言の変数への代入がエラーになる
userName = "田中";  // ReferenceError: userName is not defined

// 厳格モードがない場合、暗黙的にグローバル変数が作成されてしまう

型チェックの実施

関数の引数や戻り値の型を確認する習慣をつけましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function divide(a, b) {
  // 型チェック
  if (typeof a !== "number" || typeof b !== "number") {
    throw new TypeError("引数は数値である必要があります");
  }
  
  // ゼロ除算チェック
  if (b === 0) {
    throw new RangeError("ゼロで割ることはできません");
  }
  
  return a / b;
}

// 使用例
try {
  console.log(divide(10, 2));     // 5
  console.log(divide("10", 2));   // TypeError
  console.log(divide(10, 0));     // RangeError
} catch (error) {
  console.error(error.message);
}

早期リターンの活用

条件分岐を深くネストさせず、早期リターンで可読性を高めます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 悪い例:深いネスト
function processUser(user) {
  if (user) {
    if (user.isActive) {
      if (user.hasPermission) {
        // 実際の処理
        return doSomething(user);
      }
    }
  }
  return null;
}

// 良い例:早期リターン
function processUser(user) {
  if (!user) return null;
  if (!user.isActive) return null;
  if (!user.hasPermission) return null;
  
  return doSomething(user);
}

定数の活用

マジックナンバーやマジックストリングを避け、定数を使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 悪い例:マジックナンバー
if (user.role === 1) {
  // 管理者の処理
}

// 良い例:定数を使用
const ROLE = {
  ADMIN: 1,
  EDITOR: 2,
  VIEWER: 3
};

if (user.role === ROLE.ADMIN) {
  // 管理者の処理(意図が明確)
}

ESLintの導入

ESLintを使用すると、潜在的なバグを静的解析で検出できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// .eslintrc.json
{
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": "eslint:recommended",
  "rules": {
    "no-unused-vars": "error",
    "no-undef": "error",
    "eqeqeq": "error",
    "no-console": "warn"
  }
}

TypeScriptの検討

より厳密な型チェックが必要な場合は、TypeScriptの導入を検討しましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// TypeScriptによる型安全なコード
interface User {
  id: number;
  name: string;
  email?: string;
}

function greetUser(user: User): string {
  return `Hello, ${user.name}!`;
}

// コンパイル時に型エラーを検出
greetUser({ id: 1 });  // エラー: 'name'が必要です

デバッグのベストプラクティス

効率的にデバッグするためのベストプラクティスをまとめます。

二分探索でバグを絞り込む

大きなコードでバグを探す場合、コードの中間地点にログを入れて、バグがどちら側にあるか特定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function complexFunction(data) {
  // 処理A
  console.log("処理A完了:", data);  // まずここでチェック
  
  // 処理B
  
  // 処理C
  console.log("処理C完了:", data);  // 次にここでチェック
  
  // 処理D
  
  return result;
}

仮説を立てて検証する

やみくもにコードを変更するのではなく、仮説を立ててから検証します。

flowchart LR
    A[バグ発見] --> B[仮説を立てる]
    B --> C[検証コードを追加]
    C --> D{仮説は正しい?}
    D -->|Yes| E[修正を実施]
    D -->|No| B
    E --> F[テストで確認]

本番コードにconsole.logを残さない

デバッグが完了したら、console.log()を削除するか、開発環境でのみ出力されるようにします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 環境に応じたログ出力
const isDevelopment = process.env.NODE_ENV === "development";

function debugLog(...args) {
  if (isDevelopment) {
    console.log("[DEBUG]", ...args);
  }
}

// 使用例
debugLog("ユーザーデータ:", userData);  // 開発環境でのみ出力

まとめ

本記事では、JavaScriptのバグ対策とデバッグの基本について解説しました。

トピック ポイント
バグの種類 構文エラーは発見しやすく、論理エラーは発見が難しい
console.log ラベル付きで出力し、console.table()でオブジェクトを見やすく表示
DevTools ブレークポイントとステップ実行で効率的にデバッグ
よくあるバグ undefined、型の不一致、非同期のタイミング、スコープ、thisの参照
再現手順 環境情報、最小再現コード、期待と実際の動作を明記
予防策 厳格モード、型チェック、ESLint、TypeScriptの活用

デバッグは経験を積むほど上達します。本記事で紹介した手法を実践し、効率的なデバッグスキルを身につけてください。

参考リンク