はじめに#
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
|
console.log()だけでなく、ブラウザの開発者ツール(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の活用 |
デバッグは経験を積むほど上達します。本記事で紹介した手法を実践し、効率的なデバッグスキルを身につけてください。
参考リンク#