404 Not Found

404 Not Found


nginx

Promises および async/await の実用的な活用法

1. 学習内容


2. ストーリー:チャーリーの大量データダウンロード

チャーリーは、100個の外部APIから設定データをダウンロードする担当でした。当初、彼はforを使用してループ内でリクエストを1つずつ送信していました。各リクエストには約30秒かかったため、100件すべてを完了するには50分かかりました。その後、彼はPromise.allに切り替えて並列リクエストを行うようにしたところ、合計時間は30秒に短縮されました。しかし、サーバーから即座に429 Too Many Requestsが返されてしまいました。チャーリーは同時リクエスト数を制限する必要があることに気づき、最終的に5件に制限した結果、合計時間は約10分にまで短縮されました。これは効率的であると同時に、レート制限をトリガーしない解決策となりました。

この話は、非同期プログラミングにおける根本的なトレードオフ、すなわち速度とリソースのバランスを浮き彫りにしています。Promiseやasync/awaitは、このバランスを管理するためのツールです。


3. fs.promises API

バージョン10以降、Node.jsではfs.promisesが提供されており、これはファイルシステム操作をPromiseを返すメソッドとしてラップすることで、コールバック地獄に終止符を打っています。

(1) fsのコールバック vs fs.promises vs promisify

機能 fs コールバック fs.promises util.promisify(fs.xxx)
戻り値 void; 結果はコールバックを介して渡される Promise Promise
エラー処理 コールバックの最初のパラメータ、err try/catch または .catch() try/catch または .catch()
コードスタイル ネストされたコールバック フラットな async/await フラットな async/await
利用可能なバージョン すべてのバージョン v10以上 v8以上
代表的な使用例 fs.readFile(path, (err, data) => {}) await fs.promises.readFile(path) const readFile = promisify(fs.readFile); await readFile(path)

▶ 例:fs.promises を使ったファイルの読み書き

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

async function readAndWrite() {
  try {
    const data = await fs.readFile('input.txt', 'utf8');
    const upper = data.toUpperCase();
    await fs.writeFile('output.txt', upper);
    console.log('Done');
  } catch (err) {
    console.error('Error:', err.message);
  }
}

readAndWrite();

▶ 例:fs.promises を使用したディレクトリ操作

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

async function listFiles(dir) {
  try {
    await fs.mkdir(dir, { recursive: true });
    const files = await fs.readdir(dir);
    for (const file of files) {
      const stat = await fs.stat(`${dir}/${file}`);
      console.log(`${file} - ${stat.size} bytes`);
    }
  } catch (err) {
    console.error(err.message);
  }
}

listFiles('./my-dir');

▶ Example: util.promisify converts callback functions

JAVASCRIPT
const fs = require('fs');
const { promisify } = require('util');

const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);

async function copy() {
  const data = await readFile('source.txt', 'utf8');
  await writeFile('dest.txt', data);
  console.log('Copied');
}

copy();

4. プロミス・チェイニングの手法

4つの静的メソッドによって、複数のPromiseが互いにどのように相互作用するかが決まります。メソッドの組み合わせを誤ると、結果が大きく異なってしまう可能性があります。

(1) 4つの組み合わせ手法の比較

メソッド すべての操作が成功した場合 失敗した場合 戻り値 代表的な用途
Promise.all 結果配列全体を返す 最初の失敗でリジェクトする;セット全体をリジェクトする 結果配列 すべてのタスクが完了している必要がある
Promise.race 最初に完了した結果を返す 最初の失敗で却下 単一値 タイムアウト制御、最速の応答
Promise.allSettled すべての結果を返す 失敗を排除せず、失敗メッセージも含める {status, value/reason}[] 成功・失敗にかかわらず、すべての結果を返す
Promise.any 最初に成功した結果を返す すべてが失敗した場合のみ拒否する(AggregateError) 単一の値 複数ソース間の競合、最も速く成功したものを採用

▶ 例:Promise.all — すべてのプロミスが成功した場合にのみ成功する

JAVASCRIPT
async function fetchAll() {
  const urls = [
    'https://api.example.com/a',
    'https://api.example.com/b',
    'https://api.example.com/c',
  ];

  try {
    const results = await Promise.all(
      urls.map(url => fetch(url).then(r => r.json()))
    );
    console.log('All succeeded:', results.length);
  } catch (err) {
    console.error('One failed:', err.message);
  }
}

▶ 例:Promise.allSettled — 決してリジェクトされない

JAVASCRIPT
async function fetchAllSettled() {
  const tasks = [
    Promise.resolve({ id: 1 }),
    Promise.reject(new Error('Server down')),
    Promise.resolve({ id: 3 }),
  ];

  const results = await Promise.allSettled(tasks);

  const succeeded = results.filter(r => r.status === 'fulfilled');
  const failed = results.filter(r => r.status === 'rejected');

  console.log(`Succeeded: ${succeeded.length}, Failed: ${failed.length}`);
  failed.forEach(r => console.error('Reason:', r.reason.message));
}

▶ 例:Promise.race — タイムアウトの制御

JAVASCRIPT
function fetchWithTimeout(url, ms) {
  const fetchTask = fetch(url).then(r => r.json());
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
  );
  return Promise.race([fetchTask, timeout]);
}

async function demo() {
  try {
    const data = await fetchWithTimeout('https://api.example.com/slow', 3000);
    console.log(data);
  } catch (err) {
    console.error(err.message);
  }
}

▶ 例:Promise.any — レースコンディション下で複数のソースから成功結果を取得する

JAVASCRIPT
async function fastestMirror() {
  const mirrors = [
    fetch('https://mirror1.example.com/data').then(r => r.json()),
    fetch('https://mirror2.example.com/data').then(r => r.json()),
    fetch('https://mirror3.example.com/data').then(r => r.json()),
  ];

  try {
    const result = await Promise.any(mirrors);
    console.log('Fastest response:', result);
  } catch (err) {
    console.error('All mirrors failed:', err.errors.length);
  }
}

(2) Mermaid:プロミス・チェイニング法の実行比較

100%
flowchart TB
    subgraph all["Promise.all"]
        A1["Task A ✅"] --- A2["Task B ✅"] --- A3["Task C ✅"]
        AR["→ [A, B, C] ✅"]
    end

    subgraph race["Promise.race"]
        R1["Task A ⏱ 1s"] --- R2["Task B ⏱ 3s"] --- R3["Task C ⏱ 2s"]
        RR["→ A ✅ (Fastest)"]
    end

    subgraph settled["Promise.allSettled"]
        S1["Task A ✅"] --- S2["Task B ❌"] --- S3["Task C ✅"]
        SR["→ [{fulfilled:A}, {rejected:B}, {fulfilled:C}] ✅"]
    end

    subgraph any["Promise.any"]
        N1["Task A ❌"] --- N2["Task B ✅ ⏱ 2s"] --- N3["Task C ✅ ⏱ 3s"]
        NR["→ B ✅ (Success as Quickly as Possible)"]
    end

    all --> AR
    race --> RR
    settled --> SR
    any --> NR

    style AR fill:#c8e6c9
    style RR fill:#c8e6c9
    style SR fill:#fff9c4
    style NR fill:#c8e6c9

5. async/await を使ったエラー処理

async/await を使用すると、非同期コードが同期コードのように見えるようになり、エラー処理もそれに倣う try/catch—しかし、いくつかの落とし穴がある。

(1) エラー処理モデルの比較

パターン 実装 利点 欠点
コールバック if (err) { handle } シンプルで直感的 ネストが深いため、見落としやすい
Promise.catch() promise.then().catch() 連鎖可能、再利用可能 ネストすると依然として複雑
async/await + try/catch try { await } catch {} 同期的なスタイルで、可読性が高い await を必ず囲む必要がある
ラッパー関数 const [err, data] = await to(promise) try/catch なし、簡潔 ヘルパー関数のインポートが必要

▶ 例:非同期関数を try/catch で囲む

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

async function safeReadFile(path) {
  try {
    const data = await fs.readFile(path, 'utf8');
    return { ok: true, data };
  } catch (err) {
    return { ok: false, error: err.message };
  }
}

async function main() {
  const result = await safeReadFile('missing.txt');
  if (!result.ok) {
    console.error('Failed:', result.error);
    return;
  }
  console.log('Content:', result.data);
}

main();

▶ 例:try/catch を使用しないエラー処理関数

JAVASCRIPT
function to(promise) {
  return promise
    .then(data => [null, data])
    .catch(err => [err, null]);
}

async function main() {
  const fs = require('fs').promises;

  const [err, data] = await to(fs.readFile('config.json', 'utf8'));
  if (err) {
    console.error('Read failed:', err.message);
    return;
  }
  console.log('Config:', data);
}

main();

6. 並行処理制御

(1) 順次処理、並列処理、制約付き並行処理

実行方法 総所要時間(N個のタスク、各タスクの所要時間はT) 利点 欠点 適したシナリオ
順次実行 N × T シンプルでリソース効率が良い 処理に時間がかかる 依存関係のあるタスク
完全並列 ≈ T 最速 リソース使用率がピーク時に高くなる;スロットリングされる可能性がある 独立したタスクが少ない
並行処理数に制限がある ≈ N/並行処理数 × T 速度とリソースのバランスが取れている 実装がやや複雑 多数の独立したタスク、APIのレート制限

▶ 例:順次実行

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

async function sequential() {
  const files = ['a.txt', 'b.txt', 'c.txt'];
  const results = [];

  for (const file of files) {
    const data = await fs.readFile(file, 'utf8');
    results.push(data);
  }

  console.log('Results:', results.length);
}

▶ 例:制約付き並行制御関数

JAVASCRIPT
async function limitConcurrency(tasks, limit) {
  const results = [];
  const executing = new Set();

  for (const task of tasks) {
    const p = task().then(result => {
      executing.delete(p);
      return result;
    });
    executing.add(p);
    results.push(p);

    if (executing.size >= limit) {
      await Promise.race(executing);
    }
  }

  return Promise.all(results);
}

▶ 例:同時ダウンロード数制限 API の使用方法

JAVASCRIPT
async function fetchApi(url) {
  const res = await fetch(url);
  return res.json();
}

async function batchFetch() {
  const urls = Array.from({ length: 100 }, (_, i) =>
    `https://api.example.com/item/${i + 1}`
  );

  const tasks = urls.map(url => () => fetchApi(url));
  const results = await limitConcurrency(tasks, 5);

  console.log(`Fetched ${results.length} items`);
}

7. プロミスの連鎖に関するベストプラクティス

(1) 連鎖呼び出しに関する規則

規則 説明 反例
常に Promise を返す チェーン処理を保証する promise.then(() => { doSomething() }) 戻り値なし
エラーは常に処理する 最後に .catch() を追加する .catch() を含まない連鎖ブロックでは、エラーが捕捉されない
ネストを避ける .then() チェーンを平坦化する .then(() => { return p.then(...) })
長いチェーンを async/await に置き換える 3回以上の .then() 呼び出しを await に置き換える 5段階以上ネストされた .then() 呼び出し
注:.then() 内でスローされた例外 は、次の .catch() によって捕捉されます なぜなら、例外が処理の流れを中断しないと思っていたからです

▶ 例:フラットチェーンコール

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

function processFile(path) {
  return fs.readFile(path, 'utf8')
    .then(data => data.trim())
    .then(data => data.toUpperCase())
    .then(data => fs.writeFile('output.txt', data))
    .then(() => console.log('Saved'))
    .catch(err => console.error('Error:', err.message));
}

processFile('input.txt');

8. 包括的な例:並行処理に制限のあるバッチファイル処理ツール

ツールを作成する:ディレクトリを読み込む → 同時実行中のファイル処理を3つに制限する → 結果を集約する → 統計情報を出力する。

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

async function processFile(filePath) {
  const stat = await fs.stat(filePath);
  const content = await fs.readFile(filePath, 'utf8');
  const lines = content.split('\n').length;
  const words = content.split(/\s+/).filter(Boolean).length;
  return {
    file: path.basename(filePath),
    size: stat.size,
    lines,
    words,
  };
}

async function limitConcurrency(tasks, limit) {
  const results = [];
  const executing = new Set();

  for (const task of tasks) {
    const p = task().then(result => {
      executing.delete(p);
      return result;
    });
    executing.add(p);
    results.push(p);

    if (executing.size >= limit) {
      await Promise.race(executing);
    }
  }

  return Promise.all(results);
}

async function batchProcessDir(dirPath, concurrency = 3) {
  console.log(`Scanning directory: ${dirPath}`);

  const files = await fs.readdir(dirPath);
  const filePaths = files
    .filter(f => f.endsWith('.txt') || f.endsWith('.md') || f.endsWith('.json'))
    .map(f => path.join(dirPath, f));

  if (filePaths.length === 0) {
    console.log('No matching files found.');
    return;
  }

  const tasks = filePaths.map(fp => () => processFile(fp));
  const results = await limitConcurrency(tasks, concurrency);

  console.log('\n--- File Statistics ---');
  console.log('File'.padEnd(20) + 'Size'.padEnd(10) + 'Lines'.padEnd(8) + 'Words');
  console.log('-'.repeat(46));

  let totalLines = 0;
  let totalWords = 0;
  let totalSize = 0;

  for (const r of results) {
    console.log(
      r.file.padEnd(20) +
      String(r.size).padEnd(10) +
      String(r.lines).padEnd(8) +
      String(r.words)
    );
    totalLines += r.lines;
    totalWords += r.words;
    totalSize += r.size;
  }

  console.log('-'.repeat(46));
  console.log(
    'TOTAL'.padEnd(20) +
    String(totalSize).padEnd(10) +
    String(totalLines).padEnd(8) +
    String(totalWords)
  );
  console.log(`\nProcessed ${results.length} files (concurrency: ${concurrency})`);
}

batchProcessDir('./data', 3).catch(err => console.error('Fatal:', err.message));

パフォーマンス:

TEXT
Scanning directory: ./data

--- File Statistics ---
File                Size      Lines   Words
----------------------------------------------
config.json         256       12      42
readme.md           1024      45      312
notes.txt           512       28      178
----------------------------------------------
TOTAL               1792      85      532

Processed 3 files (concurrency: 3)

❓ よくある質問

Q Promise.all で 1 つのプロミスに失敗した場合、どうなりますか?
A 操作全体が直ちに失敗し(ファストフェイル)、最初のエラーのみが返されます。すべての結果が必要な場合は、Promise.allSettled を使用してください。
Q 並行して実行される async/await 操作の数を制限するにはどうすればよいですか?
A limitConcurrency 関数を実装し、Set を使ってアクティブな Promise を追跡し、Promise.race を使って並行操作の数を制御します。本番環境では、p-limit ライブラリを使用することもできます。
Q awaitasync 関数内でのみ使用できますか?
A はい。ES2022ではトップレベルのawaitが導入され、ESモジュールの最上位でawaitを直接使用できるようになりましたが、CommonJSモジュールでは、依然としてasync関数内にラップする必要があります。
Q util.promisify はすべてのコールバック関数を変換できますか?
A いいえ。エラー優先のコールバック、つまり (err, result) => {} 形式のコールバックでのみ機能します。カスタムの多引数コールバックは、手動でラップする必要があります。
Q Promise.race の実用的な用途にはどのようなものがありますか?
A 最も一般的な用途はタイムアウト制御です。つまり、ビジネスロジックの Promise とタイマー Promise を競わせることで、複数のリクエストの中から最も速い応答を選択することも可能です。
Q Promise.allSettled は、入力と同じ順序で結果を返しますか?
A はい。各 Promise がいつ解決されるかに関係なく、結果の配列の順序は、入力された Promise の配列の順序と厳密に一致します。
Q async 関数は何を返しますか?
A async 関数は常に Promise を返します。通常の値を返した場合でも、自動的に Promise.resolve(value) でラップされます。

📖 まとめ


📝 練習問題

  1. このレッスンにあるすべてのコード例を完成させ、それぞれが正しく動作することを確認してください。
  2. 包括的な例を修正し、独自の拡張機能を追加する
  3. 公式ドキュメントを確認し、このレッスンで取り上げられていないAPIを1~2つ見つけ、それらのテストコードを作成してください。
  4. 振り返り:このレッスンで学んだことを、実際のプロジェクトにどのように活かしますか?
  5. このレッスンで学んだことと、これまでのレッスンの内容を組み合わせて、小さなプロジェクトを作成してみてください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%