第3章 非同期処理
更新日:2025年12月17日
1. 非同期処理の3つの書き方
第1章で学んだ非同期処理の概念を復習する。同期処理では処理Aが完了するまで処理Bは開始できないが、非同期処理では処理Aの完了を待たずに処理Bを開始できる。
同期処理: 処理A完了 → 処理B開始 非同期処理: 処理A開始 → 処理B開始 → 処理A完了 → 処理B完了
Node.jsの非同期処理には歴史的に3つの書き方がある。
Table 1. 非同期処理の書き方の変遷
| 世代 | 書き方 | 登場時期 | 状態 |
|---|---|---|---|
| 第1世代 | コールバック | 2009年〜 | レガシー |
| 第2世代 | Promise | 2015年〜 | 現役(内部で使用) |
| 第3世代 | async/await | 2017年〜 | 現在の標準 |
現在はasync/awaitが主流であるが、古いコードや一部のライブラリでは第1世代・第2世代が使われているため、すべて理解しておく必要がある。
2. コールバック(第1世代)
コールバックは最初の非同期処理の書き方である。処理完了時に呼び出される関数を引数として渡す。
2.1 基本構文
fs.readFile('file.txt', (err, data) => {
if (err) {
console.log('エラー:', err);
return;
}
console.log(data);
});
2.2 コールバック地獄
コールバックの問題は、処理を連続させるとネスト(入れ子)が深くなることである。ファイルを3つ順番に読む場合を示す。
fs.readFile('file1.txt', (err, data1) => {
fs.readFile('file2.txt', (err, data2) => {
fs.readFile('file3.txt', (err, data3) => {
// ここで処理
});
});
});
このようにネストが深くなる状態を「コールバック地獄」と呼ぶ。読みにくく、保守が困難になる。この問題を解決するためにPromiseが生まれた。
3. Promise(第2世代)
Promiseはコールバック地獄を解消するために導入された。.then()メソッドをチェーンすることで、ネストを浅く保てる。
readFile('file1.txt')
.then(data1 => readFile('file2.txt'))
.then(data2 => readFile('file3.txt'))
.then(data3 => {
// ここで処理
})
.catch(err => {
console.log('エラー:', err);
});
コールバックよりは読みやすいが、まだ.then()が続いて冗長である。この問題を解決するためにasync/awaitが生まれた。
4. async/await(第3世代)
async/awaitは現在の標準的な書き方である。同期処理のように読めるコードで非同期処理を記述できる。
4.1 基本構文
async function main() {
const data1 = await fs.promises.readFile('file1.txt');
const data2 = await fs.promises.readFile('file2.txt');
const data3 = await fs.promises.readFile('file3.txt');
// ここで処理
}
main();
コールバック地獄と比較すると、格段に読みやすくなっている。
4.2 asyncとawaitの役割
Table 2. async/awaitの役割
| キーワード | 役割 | 必須条件 |
|---|---|---|
| async | この関数は非同期処理を含むと宣言 | awaitを使う関数につける |
| await | この処理の完了を待つ | async関数の中でのみ使用可能 |
awaitはasync関数の中でしか使えない。以下はエラーになる。
// ❌ エラー:awaitはasync関数の中でしか使えない
const data = await fs.promises.readFile('file.txt');
// ✅ 正しい:async関数の中で使う
async function readMyFile() {
const data = await fs.promises.readFile('file.txt');
return data;
}
4.3 awaitの動作
awaitは「この処理が終わるまで、次の行に進まない」という意味である。ただし、awaitで待っている間もNode.js全体が止まるわけではない。自分のコードの次の行には進まないが、Node.js自体は他のリクエストを処理できる。
awaitなし: readFile開始 → すぐ次の行へ → readFile完了(いつ?)
awaitあり: readFile開始 → 完了を待つ → 完了 → 次の行へ
↑
この間Node.jsは他の仕事ができる
5. エラー処理
非同期処理は失敗することがある。例えば、存在しないファイルを読もうとした場合である。JavaScriptではtry/catchを使ってエラーを処理する。
5.1 try/catch構文
async function main() {
try {
const data = await fs.promises.readFile('存在しない.txt');
console.log(data);
} catch (error) {
console.log('エラーが発生しました:', error.message);
}
}
Table 3. 言語別例外処理構文
| 言語 | 構文 |
|---|---|
| Python | try / except |
| JavaScript | try / catch |
5.2 動作の流れ
try {
処理A ← 成功したら次へ
処理B ← 成功したら次へ
処理C ← ここでエラー発生!
処理D ← 実行されない
} catch (error) {
エラー処理 ← ここに飛ぶ
}
6. 並列実行
awaitを使うと処理を順番に待つことになる。しかし、処理同士が独立している場合は、同時に実行した方が効率的である。
6.1 順番実行と並列実行
// 順番に実行(3秒)
const data1 = await readFile('file1.txt'); // 1秒待つ
const data2 = await readFile('file2.txt'); // 1秒待つ
const data3 = await readFile('file3.txt'); // 1秒待つ
// 並列実行(1秒)
const [data1, data2, data3] = await Promise.all([
readFile('file1.txt'),
readFile('file2.txt'),
readFile('file3.txt')
]);
Fig. 1 順番実行と並列実行の時間比較
順番に実行:
file1 ████████
file2 ████████
file3 ████████
→ 3秒
並列実行:
file1 ████████
file2 ████████
file3 ████████
→ 1秒
6.2 使い分け
Table 4. 順番実行と並列実行の使い分け
| 状況 | 方法 | 理由 |
|---|---|---|
| 処理Bが処理Aの結果を使う | 順番にawait | 依存関係がある |
| 処理同士が独立している | Promise.all | 同時実行で高速化 |
// 順番が必要な例
const user = await getUser(id); // まずユーザーを取得
const orders = await getOrders(user); // そのユーザーの注文を取得
// 並列でよい例
const [users, products, categories] = await Promise.all([
getUsers(), // ユーザー一覧
getProducts(), // 商品一覧
getCategories() // カテゴリ一覧
]);
7. まとめ
Table 5. 第3章のまとめ
| 概念 | 内容 |
|---|---|
| コールバック | 第1世代。ネストが深くなる問題(コールバック地獄) |
| Promise | 第2世代。.then()チェーンでネスト解消 |
| async/await | 第3世代。同期風に書ける。現在の標準 |
| async | 関数に付与。awaitを使うための宣言 |
| await | 処理完了を待つ。async関数内でのみ使用可能 |
| try/catch | エラー処理。Pythonのtry/exceptに相当 |
| Promise.all | 複数の非同期処理を並列実行 |
本コンテンツは2025年12月時点の情報に基づいて作成されています。Node.jsおよび関連ツールは活発に開発が進められており、APIや機能が変更される可能性があります。最新情報は公式ドキュメントをご確認ください。