NodeJS 速習チュートリアル

Node.js 非同期プログラミング

1. 非同期プログラミングとは?

Node.js における非同期(Asynchronous)操作とは、ファイル I/O やネットワークリクエストなどのタスク完了を待機している間に、プログラムが他の処理を並行して進めることができる仕組みです。

このノンブロッキング(Non-blocking)アプローチにより、Node.js は単一のスレッドで数千もの同時接続(Concurrent connections)を効率的に処理することが可能になります。

2. 同期(Sync)vs 非同期(Async):主な違い

特徴同期 (Synchronous)非同期 (Asynchronous)
実行の挙動完了するまで実行をブロックするブロックせずに実行を継続する
複雑さ理解しやすくシンプル制御フローがやや複雑になる
パフォーマンス遅延の原因になりやすいパフォーマンスが向上する
代表的な関数readFileSync などコールバック, Promise, async/await

2.1 例:同期的なファイルの読み込み

const fs = require('fs');

console.log('1. 同期読み込みを開始...');
// ファイルを読み込むまで次の行に進まない
const data = fs.readFileSync('myfile.txt', 'utf8');
console.log('2. ファイルの内容:', data);
console.log('3. ファイル読み込み完了');

出力順序: 1 → 2 → 3 (各ステップ間で処理がブロックされます)

2.2 例:非同期的なファイルの読み込み

const fs = require('fs');

console.log('1. 非同期読み込みを開始...');
// 読み込み完了を待たずに次の処理へ進む
fs.readFile('myfile.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log('2. ファイルの内容:', data);
});

console.log('3. 読み込み操作の開始を完了(バックグラウンドで処理中)');

出力順序: 1 → 3 → 2 (ファイルの読み込み完了を待機しません)

3. コールバック地獄(Callback Hell)の回避

非同期処理を多用すると、処理が入れ子になりコードの可読性が著しく低下する「コールバック地獄(Callback Hell)」に陥ることがあります。

3.1 問題:ネストされたコールバック

getUser(userId, (err, user) => {
  if (err) return handleError(err);
  getOrders(user.id, (err, orders) => {
    if (err) return handleError(err);
    processOrders(orders, (err) => {
      if (err) return handleError(err);
      console.log('すべての処理が完了!');
    });
  });
});

3.2 解決策:Promise(プロミス)の使用

getUser(userId)
  .then(user => getOrders(user.id))
  .then(orders => processOrders(orders))
  .then(() => console.log('すべての処理が完了!'))
  .catch(handleError);

3.3 さらに洗練された方法:Async/Await

async function processUser(userId) {
  try {
    const user = await getUser(userId);
    const orders = await getOrders(user.id);
    await processOrders(orders);
    console.log('すべての処理が完了!');
  } catch (err) {
    handleError(err);
  }
}

4. モダンな非同期パターン

4.1 Promise API の利用

const fs = require('fs').promises;

console.log('1. ファイルを読み込み中...');
fs.readFile('myfile.txt', 'utf8')
  .then(data => {
    console.log('3. ファイルの内容:', data);
  })
  .catch(err => console.error('エラー:', err));

console.log('2. これはファイルの読み込み前に実行されます!');

4.2 Async/Await(推奨される方法)

async function readFiles() {
  try {
    console.log('1. ファイルの読み込みを開始...');
    const data1 = await fs.readFile('file1.txt', 'utf8');
    const data2 = await fs.readFile('file2.txt', 'utf8');
    console.log('2. ファイルの読み込みに成功しました!');
    return { data1, data2 };
  } catch (error) {
    console.error('ファイル読み込みエラー:', error);
  }
}

5. ベストプラクティス

5.1 推奨される書き方

可読性を高めるために async/await を活用しましょう。

// async/await を使用して可読性を確保
async function getUserData(userId) {
  try {
    const user = await User.findById(userId);
    const orders = await Order.find({ userId });
    return { user, orders };
  } catch (error) {
    console.error('ユーザーデータの取得に失敗:', error);
    throw error; // エラーを再スローするか適切に処理
  }
}

5.2 避けるべき書き方

ネストされたコールバックは、メンテナンス性を著しく低下させます。

// ネストされたコールバックは読みづらく、保守が困難
User.findById(userId, (err, user) => {
  if (err) return console.error(err);
  Order.find({ userId }, (err, orders) => {
    if (err) return console.error(err);
    // 注文処理をここに記述...
  });
});

6. 例:並列実行の仕組み

互いに依存しない複数の非同期操作がある場合は、Promise.all を使用して並列に実行することで、トータルの処理時間を短縮できます。

// 複数の非同期操作を並列に実行する
async function fetchAllData() {
  try {
    // すべての Promise が解決するのを同時に待機
    const [users, products, orders] = await Promise.all([
      User.find(),
      Product.find(),
      Order.find()
    ]);
    return { users, products, orders };
  } catch (error) {
    console.error('データ取得エラー:', error);
    throw error;
  }
}

7. なぜ非同期コードを使用するのか?

非同期コードを採用することで、Node.js はファイルアクセスやデータベースクエリなどの「重い」操作の完了を待つことなく、多くのリクエストを同時に捌くことができます。これが、Node.js が Web サーバーやリアルタイムアプリケーションに非常に適している理由です。

8. まとめ

  • Node.js はイベントループを使用してノンブロッキング I/O を実現している。
  • モダンな非同期コードでは、Promise と併せて async/await を使用するのが標準。
  • 非同期操作では常に try/catch によるエラーハンドリングを忘れないこと。
  • 独立した操作は Promise.all で並列化し、効率を最大化する。
  • 適切な非同期パターンを選択し、コールバック地獄を回避する。