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

従来の同期(Synchronous)プログラミングでは、コードは順番に実行され、各行のコードは前の行のコードの実行が完了するまで待つ必要があります。一方、非同期プログラミング(Asynchronous)では、コードが特定の操作(I/O 操作など)の完了を待つ間に他のタスクを実行し続けることができ、プログラムの実行をブロックしません。

Node.js は非同期 I/O モデルを採用しており、これにより高並行のネットワークアプリケーションの処理に特に適しています。非同期プログラミングを理解することは Node.js を習得する鍵です。


なぜ Node.js に非同期プログラミングが必要か

Node.js はシングルスレッドであり、つまり一度に1つのタスクしか実行できません。同期プログラミング方式を使用すると、時間のかかる操作(大きなファイルの読み取りやネットワークリクエストなど)が実行されている間、プログラム全体がブロックされ、他のリクエストを処理できなくなります。

非同期プログラミングはこの問題を解決します:

  • アプリケーションのスループットを向上させる
  • システムリソースをより効率的に利用する
  • より良いユーザーエクスペリエンスを提供する

Node.js 非同期プログラミングの3つの主要な方法

1、コールバック関数 (Callbacks)

コールバック関数は Node.js で最も基本的な非同期処理方式です。関数を別の関数に引数として渡し、操作の完了後にこの関数を呼び出します。

例

const fs = require('fs');

// ファイルを非同期で読み込む
fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('ファイル読み込みエラー:', err);
    return;
  }
  console.log('ファイルの内容:', data);
});

console.log('ファイル読み込みリクエストを送信しました。他のコードを実行し続けます...');

特徴:

  • シンプルで直接的
  • 「コールバック地獄」(Callback Hell) を引き起こしやすい
  • エラー処理が直感的でない

2、Promise

Promise は ES6 で導入された非同期プログラミングのソリューションであり、非同期操作の最終的な完了または失敗とその結果値を表します。

例

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

// Promise を使用してファイルを読み込む
fs.readFile('example.txt', 'utf8')
  .then(data => {
    console.log('ファイルの内容:', data);
  })
  .catch(err => {
    console.error('ファイル読み込みエラー:', err);
  });

console.log('ファイル読み込みリクエストを送信しました。他のコードを実行し続けます...');

特徴:

  • チェーン呼び出しが可能で、コールバック地獄を回避できる
  • より優れたエラー処理メカニズム
  • 状態は不可逆(pending → fulfilled または rejected)

3、async/await

async/await は ES8 で導入されたシンタックスシュガーで、Promise に基づいており、非同期コードを同期コードのように見せます。

例

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

async function readFile() {
  try {
    const data = await fs.readFile('example.txt', 'utf8');
    console.log('ファイルの内容:', data);
  } catch (err) {
    console.error('ファイル読み込みエラー:', err);
  }
}

readFile();
console.log('ファイル読み込みリクエストを送信しました。他のコードを実行し続けます...');

特徴:

  • コードがより簡潔で読みやすい
  • エラー処理には try/catch 構造を使用する
  • async 関数内で使用する必要がある

エラー処理のベストプラクティス

コールバック関数でのエラー処理

例

fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('ファイル読み込みエラー:', err);
    return; // 重要:エラー処理後は return し、後続のコードを実行しないようにする
  }
  console.log('ファイルの内容:', data);
});

Promise のエラー処理

例

fs.readFile('example.txt', 'utf8')
  .then(data => {
    console.log('ファイルの内容:', data);
    return someOtherAsyncOperation(data);
  })
  .then(result => {
    console.log('2番目のステップの結果:', result);
  })
  .catch(err => {
    console.error('処理中にエラーが発生しました:', err);
  });

async/await のエラー処理

例

async function processData() {
  try {
    const data = await fs.readFile('example.txt', 'utf8');
    const result = await someOtherAsyncOperation(data);
    console.log('最終結果:', result);
  } catch (err) {
    console.error('処理中にエラーが発生しました:', err);
  }
}

processData();

パフォーマンスに関する考慮事項

  1. イベントループのブロッキングを避ける:長時間実行される同期コードはアプリ全体をブロックします
  2. 非同期を適切に使用する:すべての操作に非同期が必要なわけではなく、単純な計算タスクでは同期方式の方が効率的な場合があります
  3. 並行数を制御する:過剰な並行非同期操作はリソースの枯渇を引き起こす可能性があります
  4. ストリームを使用して大きなファイルを処理する:大きなファイルには一括読み込みではなくストリーム(Stream)を使用します
その他の拡張