【JS応用】Promise チェーン

Promise チェーンの極意:非同期処理を制御するモダンな設計手法

JavaScriptにおける非同期処理の歴史は、コールバック地獄との戦いの歴史でした。Promiseの登場は、その複雑な制御フローを「チェーン」という直感的な構造へと昇華させました。本稿では、Promise チェーンの内部構造から、実務で遭遇する複雑なユースケース、そしてエラーハンドリングのベストプラクティスまで、フロントエンドエンジニアが知っておくべき深淵を解説します。

Promise チェーンの基本概念と実行メカニズム

Promise チェーンとは、`.then()` メソッドを連続して呼び出すことで、非同期処理を逐次実行する設計パターンです。この仕組みの核心は、`.then()` が常に新しい Promise インスタンスを返すという点にあります。

多くの開発者が誤解している点として、「`.then()` に渡されたコールバックが値を返すと、その値で新しい Promise が解決される」という仕様があります。もしコールバック内で別の Promise を返せば、チェーンはその Promise が解決されるまで待機します。これが、非同期処理を同期処理のように記述できる理由です。

チェーンの各ステップは独立したコンテキストを持ち、前のステップの結果を次のステップへ渡す「パイプライン」のような役割を果たします。この構造により、深いネストを回避し、コードの可読性を劇的に向上させることが可能です。

サンプルコード:複雑な非同期フローの構築

以下に、APIからユーザー情報を取得し、そのユーザーの投稿をフェッチし、最後にコメントを抽出するという現実的なシナリオを示します。


// ユーザー取得をシミュレート
const fetchUser = (id) => Promise.resolve({ id, name: 'Alice' });

// 投稿取得をシミュレート
const fetchPosts = (user) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([{ id: 1, title: 'Hello World' }, { id: 2, title: 'Promise Chain' }]);
    }, 500);
  });
};

// 投稿からコメントを取得する処理
const fetchComments = (posts) => {
  return Promise.resolve(posts.map(p => ({ postId: p.id, comment: 'Great post!' })));
};

// Promise チェーンによるフロー制御
fetchUser(1)
  .then(user => {
    console.log('User fetched:', user.name);
    return fetchPosts(user); // Promiseを返すことで待機する
  })
  .then(posts => {
    console.log('Posts fetched:', posts.length);
    return fetchComments(posts);
  })
  .then(comments => {
    console.log('Comments processed:', comments);
  })
  .catch(error => {
    console.error('Error occurred in pipeline:', error);
  })
  .finally(() => {
    console.log('Process completed.');
  });

このコードにおいて重要なのは、各 `.then()` ブロックが前の Promise の結果を次の引数として受け取っている点です。また、途中で例外が発生した場合、その後の `.then()` はスキップされ、直近の `.catch()` へ制御が移ります。

実務における注意点とアンチパターン

実務で Promise チェーンを扱う際、最も頻繁に発生するミスが「チェーンの分断」です。

1. チェーンの途中で return を忘れる
`.then()` 内で処理を行っても、値を `return` しなければ、次の `.then()` には `undefined` が渡されます。これは非同期処理の連携を壊す最大の要因です。

2. 過度なネストの混入
Promise チェーンを使っているにもかかわらず、`.then()` の中でさらに `new Promise` を作成してネストさせるのは避けましょう。これは「Promise コンストラクタアンチパターン」と呼ばれます。関数を適切に分割し、Promise を返す関数を呼び出す形にリファクタリングすべきです。

3. エラーハンドリングの欠如
チェーンの最後に `.catch()` を置くだけでは不十分な場合があります。特定のステップで失敗した際に、そのステップ固有のリカバリー処理(デフォルト値の適用など)を行いたい場合は、各 `.then()` に個別のエラーハンドリングを仕込むか、チェーンの中間でエラーを捕捉して解決する設計が必要です。

async/await との共存と使い分け

現代のフロントエンド開発では、`async/await` 構文が主流です。`async/await` は内部的には Promise チェーンのシンタックスシュガーです。しかし、Promise チェーンが完全に不要になったわけではありません。

例えば、複数の非同期処理を並列実行する場合、`Promise.all()` を用いたチェーンの方が直感的です。


// 並列実行の例
async function loadDashboard() {
  try {
    const [user, posts, settings] = await Promise.all([
      fetchUser(1),
      fetchPosts(1),
      fetchSettings(1)
    ]);
    // ここでまとめて処理
    return { user, posts, settings };
  } catch (err) {
    // 全てのエラーを一括で捕捉
    handleError(err);
  }
}

このように、逐次処理には `async/await` を、並列処理や複雑な分岐フローには Promise チェーンの特性を活かすという使い分けが、洗練されたエンジニアのスタイルです。

高度なテクニック:チェーンの動的生成

実務では、動的に非同期処理の数を決定しなければならないケースがあります。例えば、ユーザーが選択した複数のファイルを順番にアップロードする場合などです。この場合、`Array.prototype.reduce()` を用いて Promise チェーンを動的に構築するテクニックが非常に有用です。


const tasks = [task1, task2, task3];

// reduceを使ってPromiseを連結する
const runSequentially = tasks.reduce((promiseChain, currentTask) => {
  return promiseChain.then(results => {
    return currentTask().then(result => [...results, result]);
  });
}, Promise.resolve([]));

runSequentially.then(allResults => console.log('Finished:', allResults));

この手法は、配列の要素数や内容が実行時まで分からない場合において、強力な制御フローを提供します。

まとめ:Promise チェーンを使いこなすということ

Promise チェーンは単なる非同期処理の連結手段ではなく、アプリケーションのデータフローを制御するための強力なフレームワークです。

1. `.then()` は常に新しい Promise を返し、パイプラインを形成する。
2. `return` を忘れないことで、データの流れを維持する。
3. エラーハンドリングは、チェーンの終端だけでなく、必要に応じて局所的にも適用する。
4. `async/await` との特性を理解し、並列処理には `Promise.all()` 等のユーティリティを活用する。

これらの原則を守ることで、複雑なフロントエンドの非同期処理も、保守性が高く堅牢なコードとして記述できるようになります。Promise チェーンの挙動を深く理解することは、JavaScript の非同期モデルを掌握することと同義であり、それはシニアエンジニアへの登竜門と言えるでしょう。

非同期処理を恐れず、Promise という強力な武器を使いこなし、ユーザーに最高の体験を提供し続けてください。コードは常に「読みやすさ」と「予測可能性」を最優先に設計することが、長期的な開発における成功の鍵となります。

コメント

タイトルとURLをコピーしました