T-CREATOR

Node.js デバッグ術:console・Node Inspector・VSCode 連携

Node.js デバッグ術:console・Node Inspector・VSCode 連携

Node.js アプリケーションの開発において、効率的なデバッグ技術の習得は必要不可欠なスキルです。本記事では、基本的なconsole系デバッグから始まり、Node Inspector、そして VSCode との連携まで、段階的にスキルアップできる実践的なデバッグ手法をご紹介します。

実際のエラーケースやコード例を豊富に含めているため、現場でそのまま活用できる内容となっています。デバッグに悩む初心者の方から、より効率的なワークフローを求める中級者の方まで、きっと新しい発見があることでしょう。

Node.js デバッグの基礎知識

デバッグとは何か

デバッグは、プログラムの動作を観察し、予期しない挙動やエラーの原因を特定・修正するプロセスです。Node.js アプリケーションでは、以下のような問題を解決する必要があります。

#問題の種類対処方法
1ランタイムエラーconsole.error、Node Inspector
2ロジックエラーブレークポイント、ステップ実行
3パフォーマンス問題プロファイリング、メモリ監視
4非同期処理エラーPromise/async await デバッグ

Node.js 特有のデバッグ課題

Node.js には他の環境とは異なる特有の課題があります。

非同期処理とコールバック地獄

Node.js の非同期処理では、エラーの発生箇所とスタックトレースが一致しないことがよくあります。

javascript// よくあるエラーパターン:コールバック内でのエラー
const fs = require('fs');

function readMultipleFiles(callback) {
  fs.readFile('file1.txt', 'utf8', (err, data1) => {
    if (err) {
      callback(err);
      return;
    }

    fs.readFile('file2.txt', 'utf8', (err, data2) => {
      if (err) {
        callback(err);
        return;
      }

      // この時点でエラーが発生した場合の追跡が困難
      const result =
        data1.toUpperCase() + data2.toLowerCase();
      callback(null, result);
    });
  });
}

このようなコードでは、エラーが発生した際のデバッグが困難になります。スタックトレースが複雑になり、どこで実際に問題が起きているかを特定するのに時間がかかってしまうのです。

メモリリークの検出困難

Node.js アプリケーションは長時間稼働することが多く、メモリリークが深刻な問題となります。

javascript// メモリリークの原因となりやすいパターン
const EventEmitter = require('events');
const emitter = new EventEmitter();

function createLeakyFunction() {
  const largeArray = new Array(1000000).fill('data'); // 大きなデータ

  // イベントリスナーが削除されずに残ってしまう
  emitter.on('event', () => {
    console.log(largeArray.length);
  });
}

// 繰り返し実行されるとメモリリークが発生
setInterval(createLeakyFunction, 1000);

console 系デバッグ手法の活用

console.log/error/warn/table の使い分け

基本的なデバッグ手法として、consoleオブジェクトの各メソッドを適切に使い分けることが重要です。

javascript// console.log:一般的な情報出力
console.log('アプリケーション開始');
console.log('ユーザーID:', userId);

// console.error:エラー情報(赤色で表示)
console.error('データベース接続エラー:', err.message);

// console.warn:警告情報(黄色で表示)
console.warn('非推奨のAPIを使用しています');

// console.info:情報レベルのログ
console.info('設定ファイルを読み込みました');

console.table による構造化データの表示

オブジェクトや配列のデータを見やすく表示したい場合は、console.tableが非常に便利です。

javascript// ユーザーデータの表示例
const users = [
  {
    id: 1,
    name: '田中太郎',
    email: 'tanaka@example.com',
    status: 'active',
  },
  {
    id: 2,
    name: '佐藤花子',
    email: 'sato@example.com',
    status: 'inactive',
  },
  {
    id: 3,
    name: '鈴木一郎',
    email: 'suzuki@example.com',
    status: 'active',
  },
];

console.table(users);
// テーブル形式で整理された出力が得られる

// 特定のプロパティのみ表示
console.table(users, ['name', 'status']);

効果的なログ出力のテクニック

デバッグ効率を上げるためのログ出力テクニックをご紹介します。

構造化ログと JSON 出力

javascript// 基本的な構造化ログ
function logWithContext(level, message, context = {}) {
  const logEntry = {
    timestamp: new Date().toISOString(),
    level: level,
    message: message,
    context: context,
    pid: process.pid,
  };

  console.log(JSON.stringify(logEntry, null, 2));
}

// 使用例
logWithContext('ERROR', 'ユーザー認証に失敗', {
  userId: 12345,
  ip: '192.168.1.100',
  userAgent: 'Mozilla/5.0...',
});

条件付きログ出力

本番環境とデバッグ環境でログ出力を切り替える仕組みを実装できます。

javascript// 環境変数によるログレベル制御
const LOG_LEVEL = process.env.LOG_LEVEL || 'info';

const logger = {
  debug: (message, data) => {
    if (['debug'].includes(LOG_LEVEL)) {
      console.log(`[DEBUG] ${message}`, data);
    }
  },

  info: (message, data) => {
    if (['debug', 'info'].includes(LOG_LEVEL)) {
      console.info(`[INFO] ${message}`, data);
    }
  },

  error: (message, data) => {
    console.error(`[ERROR] ${message}`, data);
  },
};

// 使用例
logger.debug('データベースクエリ実行', {
  query: 'SELECT * FROM users',
});
logger.info('サーバー起動完了', { port: 3000 });
logger.error('接続エラー', { error: 'ECONNREFUSED' });

パフォーマンス測定の console.time 活用

処理時間の測定は、パフォーマンス問題の特定に欠かせません。

javascript// 基本的な処理時間測定
console.time('データベース接続');
// データベース接続処理
await connectToDatabase();
console.timeEnd('データベース接続');

// 複数の処理時間を同時測定
console.time('ファイル読み込み');
console.time('データ変換');

const fileData = await fs.readFile(
  'large-file.json',
  'utf8'
);
console.timeEnd('ファイル読み込み');

const parsedData = JSON.parse(fileData);
console.timeEnd('データ変換');

高度なパフォーマンス測定

より詳細なパフォーマンス分析には、console.timeLogperformanceAPI を組み合わせます。

javascriptconst { performance } = require('perf_hooks');

async function measureDatabaseOperations() {
  console.time('全体処理');

  // 接続開始
  console.time('接続');
  const db = await connectToDatabase();
  console.timeEnd('接続');

  // クエリ実行
  console.time('クエリ実行');
  const startTime = performance.now();

  const users = await db
    .collection('users')
    .find({})
    .toArray();

  const endTime = performance.now();
  console.timeEnd('クエリ実行');

  console.log(
    `詳細実行時間: ${(endTime - startTime).toFixed(2)}ms`
  );
  console.log(`取得件数: ${users.length}件`);

  console.timeEnd('全体処理');
}

Node Inspector によるプロフェッショナルデバッグ

Node Inspector のセットアップ方法

Node Inspector は Chrome DevTools ライクなデバッグ体験を提供する強力なツールです。

基本的なセットアップ

bash# Node.js v6.3.0以降では内蔵されているため、特別なインストールは不要

# デバッグモードでNode.jsアプリケーションを起動
node --inspect app.js

# 特定のポートを指定する場合
node --inspect=0.0.0.0:9229 app.js

# アプリケーション開始時に即座にブレークポイントで停止
node --inspect-brk app.js

アプリケーションを起動すると、以下のようなメッセージが表示されます:

csharpDebugger listening on ws://127.0.0.1:9229/12345678-1234-1234-1234-123456789abc
For help, see: https://nodejs.org/en/docs/inspector

Chrome DevTools での接続

  1. Chrome ブラウザで chrome:​/​​/​inspect にアクセス
  2. "Remote Target" セクションでアプリケーションを確認
  3. "inspect" リンクをクリックしてデバッガーを開く

ブレークポイントとステップ実行

Node Inspector の最も基本的で重要な機能がブレークポイントとステップ実行です。

ブレークポイントの設定

javascript// express.js アプリケーションの例
const express = require('express');
const app = express();

app.get('/users/:id', async (req, res) => {
  try {
    const userId = req.params.id;

    // ここにブレークポイントを設定
    console.log('ユーザーID取得:', userId);

    // データベースからユーザー情報を取得
    const user = await getUserById(userId);

    if (!user) {
      // エラーハンドリングのブレークポイント
      return res
        .status(404)
        .json({ error: 'User not found' });
    }

    res.json(user);
  } catch (error) {
    // 例外処理のブレークポイント
    console.error('ユーザー取得エラー:', error);
    res
      .status(500)
      .json({ error: 'Internal server error' });
  }
});

デバッガー文を使用したプログラムからのブレークポイント

コード内に直接ブレークポイントを設定することも可能です。

javascriptasync function processUserData(userData) {
  // データ検証
  if (!userData || !userData.email) {
    debugger; // ここで実行が停止
    throw new Error('Invalid user data');
  }

  // メール形式のチェック
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(userData.email)) {
    debugger; // メール形式エラー時に停止
    throw new Error('Invalid email format');
  }

  return userData;
}

メモリ使用量と CPU プロファイリング

Node Inspector では、メモリ使用量や CPU 使用率の詳細な分析が可能です。

メモリリークの検出

javascript// メモリリークの可能性があるコード例
const express = require('express');
const app = express();

// グローバル変数にデータが蓄積される問題
let userSessions = {};
let requestLog = [];

app.get('/login', (req, res) => {
  const sessionId = generateSessionId();
  const userData = {
    id: req.body.userId,
    loginTime: new Date(),
    ipAddress: req.ip,
    userAgent: req.get('User-Agent'),
  };

  // セッションデータが削除されずに蓄積
  userSessions[sessionId] = userData;

  // リクエストログも無制限に蓄積
  requestLog.push({
    timestamp: new Date(),
    method: req.method,
    url: req.url,
    sessionId: sessionId,
  });

  res.json({ sessionId: sessionId });
});

メモリリークの修正版

javascript// 修正版:適切なメモリ管理
const express = require('express');
const app = express();

// Map を使用し、サイズ制限を設ける
const userSessions = new Map();
const requestLog = [];
const MAX_SESSIONS = 10000;
const MAX_LOG_ENTRIES = 1000;

app.get('/login', (req, res) => {
  const sessionId = generateSessionId();
  const userData = {
    id: req.body.userId,
    loginTime: new Date(),
    ipAddress: req.ip,
    userAgent: req.get('User-Agent'),
  };

  // セッション数の制限
  if (userSessions.size >= MAX_SESSIONS) {
    // 最も古いセッションを削除
    const oldestKey = userSessions.keys().next().value;
    userSessions.delete(oldestKey);
  }

  userSessions.set(sessionId, userData);

  // ログエントリ数の制限
  if (requestLog.length >= MAX_LOG_ENTRIES) {
    requestLog.shift(); // 最も古いログを削除
  }

  requestLog.push({
    timestamp: new Date(),
    method: req.method,
    url: req.url,
    sessionId: sessionId,
  });

  res.json({ sessionId: sessionId });
});

// セッション削除のエンドポイント
app.post('/logout', (req, res) => {
  const sessionId = req.body.sessionId;
  if (userSessions.has(sessionId)) {
    userSessions.delete(sessionId);
  }
  res.json({ message: 'Logged out successfully' });
});

VSCode 連携でさらに効率的なデバッグ環境

VSCode の Node.js デバッグ設定

VSCode は Node.js デバッグに特化した優れた機能を提供しています。

基本的なデバッグ設定

.vscode​/​launch.json ファイルを作成し、デバッグ設定を行います。

json{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Node.js アプリケーション起動",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/app.js",
      "console": "integratedTerminal",
      "env": {
        "NODE_ENV": "development",
        "DEBUG": "*"
      }
    },
    {
      "name": "Node.js プロセスにアタッチ",
      "type": "node",
      "request": "attach",
      "port": 9229,
      "restart": true,
      "localRoot": "${workspaceFolder}",
      "remoteRoot": "."
    }
  ]
}

launch.json の設定方法

様々なシナリオに対応した launch.json の設定例をご紹介します。

Express.js アプリケーション用設定

json{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Express サーバー起動",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/server.js",
      "env": {
        "NODE_ENV": "development",
        "PORT": "3000",
        "DB_HOST": "localhost",
        "DB_PORT": "5432"
      },
      "console": "integratedTerminal",
      "restart": true,
      "runtimeArgs": ["--inspect"],
      "skipFiles": [
        "<node_internals>/**",
        "node_modules/**"
      ]
    }
  ]
}

npm scripts と連携したデバッグ設定

json{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "npm start デバッグ",
      "type": "node",
      "request": "launch",
      "runtimeExecutable": "yarn",
      "runtimeArgs": ["start:debug"],
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen",
      "port": 9229
    },
    {
      "name": "テスト実行",
      "type": "node",
      "request": "launch",
      "runtimeExecutable": "yarn",
      "runtimeArgs": ["test", "--inspect-brk"],
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen",
      "port": 9229
    }
  ]
}

対応する package.json の scripts セクション:

json{
  "scripts": {
    "start": "node app.js",
    "start:debug": "node --inspect app.js",
    "test": "jest",
    "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand"
  }
}

デバッグコンソールの活用術

VSCode のデバッグコンソールでは、実行中のコンテキストで任意の JavaScript コードを実行できます。

変数の確認と操作

javascript// アプリケーションコード例
function calculateTotal(items) {
  let total = 0;
  let taxRate = 0.1;

  for (let item of items) {
    total += item.price * item.quantity;
  }

  // ここにブレークポイントを設定
  const totalWithTax = total * (1 + taxRate);
  return totalWithTax;
}

// デバッグコンソールで実行可能なコマンド例:
// items.length
// total
// items.map(item => item.name)
// Math.round(totalWithTax)

リアルタイムでのオブジェクト探索

javascript// Express.js のリクエストオブジェクトを探索
app.get('/api/users', (req, res) => {
  // ここでブレークポイントを設定

  // デバッグコンソールで以下を実行:
  // req.headers
  // req.query
  // req.params
  // req.body
  // Object.keys(req)

  res.json({ message: 'Users API' });
});

実践的なデバッグシナリオ

Express.js アプリケーションのデバッグ

実際の Express.js アプリケーションでよく発生する問題とその解決方法をご紹介します。

よくあるルーティングエラー

javascriptconst express = require('express');
const app = express();

// JSON パースミドルウェア
app.use(express.json());

// ユーザー取得API
app.get('/api/users/:id', async (req, res) => {
  try {
    const userId = req.params.id;

    // よくあるエラー:文字列を数値として扱う
    if (userId < 1) {
      // String < Number の比較エラー
      return res.status(400).json({
        error: 'Invalid user ID',
        received: userId,
        type: typeof userId,
      });
    }

    const user = await getUserById(parseInt(userId));
    res.json(user);
  } catch (error) {
    console.error('Error in /api/users/:id:', error);
    res.status(500).json({
      error: 'Internal server error',
      message: error.message,
      stack:
        process.env.NODE_ENV === 'development'
          ? error.stack
          : undefined,
    });
  }
});

修正版:適切な型チェック

javascriptapp.get('/api/users/:id', async (req, res) => {
  try {
    const userId = req.params.id;

    // 数値変換と検証
    const numericUserId = parseInt(userId, 10);

    if (isNaN(numericUserId) || numericUserId < 1) {
      return res.status(400).json({
        error: 'Invalid user ID',
        message: 'User ID must be a positive integer',
        received: userId,
      });
    }

    const user = await getUserById(numericUserId);

    if (!user) {
      return res.status(404).json({
        error: 'User not found',
        userId: numericUserId,
      });
    }

    res.json(user);
  } catch (error) {
    console.error('Error in /api/users/:id:', {
      error: error.message,
      stack: error.stack,
      userId: req.params.id,
      timestamp: new Date().toISOString(),
    });

    res.status(500).json({
      error: 'Internal server error',
      requestId: req.headers['x-request-id'] || 'unknown',
    });
  }
});

非同期処理エラーの特定と解決

Node.js では非同期処理のエラーハンドリングが特に重要です。

Promise の未処理エラー

javascript// 問題のあるコード:Promise の reject が処理されていない
async function fetchUserData(userId) {
  // このPromiseがrejectされた場合、UnhandledPromiseRejectionWarning が発生
  const userData = await fetch(`/api/users/${userId}`).then(
    (response) => response.json()
  );

  return userData;
}

// プロセス全体でのエラーハンドリング
process.on('unhandledRejection', (reason, promise) => {
  console.error(
    'Unhandled Rejection at:',
    promise,
    'reason:',
    reason
  );
  // アプリケーション終了の検討
  process.exit(1);
});

process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error);
  process.exit(1);
});

適切なエラーハンドリング

javascript// 修正版:適切なエラーハンドリング
async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);

    if (!response.ok) {
      throw new Error(
        `HTTP ${response.status}: ${response.statusText}`
      );
    }

    const userData = await response.json();
    return userData;
  } catch (error) {
    console.error('ユーザーデータ取得エラー:', {
      userId,
      error: error.message,
      stack: error.stack,
    });

    // エラーを再スローして呼び出し元で処理
    throw new Error(
      `Failed to fetch user data for ID ${userId}: ${error.message}`
    );
  }
}

// 使用例
async function handleUserRequest(req, res) {
  try {
    const userData = await fetchUserData(req.params.id);
    res.json(userData);
  } catch (error) {
    res.status(500).json({
      error: 'Failed to retrieve user data',
      message: error.message,
    });
  }
}

async/await でのエラー処理パターン

javascript// 複数の非同期処理を含む関数
async function processUserRegistration(userData) {
  let user = null;
  let profile = null;

  try {
    // ユーザー作成
    user = await createUser(userData);
    console.log('ユーザー作成完了:', user.id);

    // プロフィール作成
    profile = await createUserProfile(
      user.id,
      userData.profile
    );
    console.log('プロフィール作成完了:', profile.id);

    // メール送信
    await sendWelcomeEmail(user.email);
    console.log('ウェルカムメール送信完了');

    return { user, profile };
  } catch (error) {
    console.error('ユーザー登録処理エラー:', {
      error: error.message,
      step: getErrorStep(error),
      userData: { email: userData.email }, // 機密情報は除外
    });

    // ロールバック処理
    if (profile) {
      await deleteUserProfile(profile.id).catch(
        console.error
      );
    }
    if (user) {
      await deleteUser(user.id).catch(console.error);
    }

    throw error;
  }
}

function getErrorStep(error) {
  if (error.message.includes('create user'))
    return 'user_creation';
  if (error.message.includes('profile'))
    return 'profile_creation';
  if (error.message.includes('email'))
    return 'email_sending';
  return 'unknown';
}

TypeScript プロジェクトでのデバッグ設定

TypeScript プロジェクトでは、ソースマップを活用したデバッグが重要です。

tsconfig.json の設定

json{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "sourceMap": true,
    "inlineSourceMap": false,
    "inlineSources": false
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

TypeScript 用 launch.json

json{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "TypeScript Node.js",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/src/app.ts",
      "preLaunchTask": "tsc: build",
      "outFiles": ["${workspaceFolder}/dist/**/*.js"],
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen",
      "skipFiles": ["<node_internals>/**"],
      "env": {
        "NODE_ENV": "development"
      }
    },
    {
      "name": "TypeScript 直接実行",
      "type": "node",
      "request": "launch",
      "runtimeArgs": ["-r", "ts-node/register"],
      "args": ["${workspaceFolder}/src/app.ts"],
      "env": {
        "NODE_ENV": "development"
      },
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen"
    }
  ]
}

tasks.json の設定

json{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "tsc: build",
      "type": "typescript",
      "tsconfig": "tsconfig.json",
      "option": "watch",
      "problemMatcher": ["$tsc-watch"],
      "group": {
        "kind": "build",
        "isDefault": true
      }
    }
  ]
}

TypeScript デバッグの実例

typescript// src/services/UserService.ts
interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

class UserService {
  private users: User[] = [];

  async createUser(
    userData: Omit<User, 'id' | 'createdAt'>
  ): Promise<User> {
    // デバッグポイント:型チェックとバリデーション
    if (!userData.name || !userData.email) {
      throw new Error('Name and email are required');
    }

    const user: User = {
      id: this.users.length + 1,
      name: userData.name,
      email: userData.email,
      createdAt: new Date(),
    };

    // デバッグポイント:ユーザー作成プロセス
    this.users.push(user);

    return user;
  }

  async getUserById(id: number): Promise<User | undefined> {
    // デバッグポイント:検索ロジック
    return this.users.find((user) => user.id === id);
  }
}

export default UserService;

まとめ

本記事では、Node.js アプリケーションの効率的なデバッグ手法について、基本的なconsole系メソッドから高度な Node Inspector、そして VSCode 連携まで段階的にご紹介いたしました。

重要なポイントの振り返り

#デバッグ手法適用場面メリット
1console 系基本的な動作確認簡単、高速
2Node Inspector詳細な動作解析高機能、視覚的
3VSCode 連携統合開発環境効率的、拡張性

デバッグスキルの向上は一朝一夕では身につきませんが、今回ご紹介した手法を実際の開発で活用することで、確実にスキルアップできるでしょう。特に、エラーが発生した際に慌てず、適切なツールを選択して原因を特定できるようになることが重要です。

最初は基本的なconsole.logから始めて、徐々により高度なデバッグ手法にチャレンジしていってください。そして、チーム開発では一貫したデバッグ環境の構築も忘れずに行いましょう。

効率的なデバッグ技術を身につけることで、開発生産性が大幅に向上し、より品質の高い Node.js アプリケーションを開発できるようになることでしょう。

関連リンク