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 で並列化し、効率を最大化する。
- 適切な非同期パターンを選択し、コールバック地獄を回避する。