第3章 非同期処理

更新日:2025年12月17日

本章では、Node.jsにおける非同期処理の書き方を解説する。第1章で概念を学んだ非同期処理を、実際のコードでどう記述するかを説明する。コールバック、Promise、async/awaitという3つの書き方の歴史と、現在の標準であるasync/awaitの使い方、エラー処理、並列実行について学ぶ。

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や機能が変更される可能性があります。最新情報は公式ドキュメントをご確認ください。