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('ファイル読み込みリクエストを送信しました。他のコードを実行し続けます...');
// ファイルを非同期で読み込む
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('ファイル読み込みリクエストを送信しました。他のコードを実行し続けます...');
// 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('ファイル読み込みリクエストを送信しました。他のコードを実行し続けます...');
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);
});
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);
});
.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();
try {
const data = await fs.readFile('example.txt', 'utf8');
const result = await someOtherAsyncOperation(data);
console.log('最終結果:', result);
} catch (err) {
console.error('処理中にエラーが発生しました:', err);
}
}
processData();
パフォーマンスに関する考慮事項
- イベントループのブロッキングを避ける:長時間実行される同期コードはアプリ全体をブロックします
- 非同期を適切に使用する:すべての操作に非同期が必要なわけではなく、単純な計算タスクでは同期方式の方が効率的な場合があります
- 並行数を制御する:過剰な並行非同期操作はリソースの枯渇を引き起こす可能性があります
- ストリームを使用して大きなファイルを処理する:大きなファイルには一括読み込みではなくストリーム(Stream)を使用します