Promise チェーンで処理をつなぐ

Promise の真価は .then() をチェーンすることで発揮される。複数の非同期処理を順番に実行したいとき、コールバックではネストが深くなるが、Promise チェーンならフラットに記述できる。コールバック地獄に対する直接的な解決策がこの仕組みだ。

then はなぜチェーンできるのか

.then() メソッドは新しい Promise を返す。この性質があるからこそ、.then() の後にさらに .then() をつなげられる。

const promise = new Promise(function(resolve) {
  resolve(1);
});

promise
  .then(function(value) {
    console.log(value); // 1
    return value + 1;
  })
  .then(function(value) {
    console.log(value); // 2
    return value + 1;
  })
  .then(function(value) {
    console.log(value); // 3
  });

.then() のコールバックで値を return すると、その値が次の .then() に渡される。同期的な値を返しても、Promise でラップされて次に伝播する仕組みだ。

コールバック地獄との比較

前の記事で見たコールバック地獄のコードを、Promise チェーンで書き直してみよう。

コールバック地獄

ネストが深くなり、処理の流れを追うのが困難。エラーハンドリングも分散する。

Promise チェーン

フラットに並び、上から下へ順番に読める。エラーは末尾の .catch() でまとめて処理できる。

コールバック方式ではこうだった。

getUser(userId, function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetail(orders[0].id, function(detail) {
      console.log(detail);
    });
  });
});

Promise チェーンではこう書ける。

getUser(userId)
  .then(function(user) {
    return getOrders(user.id);
  })
  .then(function(orders) {
    return getOrderDetail(orders[0].id);
  })
  .then(function(detail) {
    console.log(detail);
  })
  .catch(function(error) {
    console.error("エラー:", error.message);
  });

ネストが消え、処理が縦に並んで流れが一目でわかる。どの段階でエラーが起きても最後の .catch() で捕捉される点も大きい。

then で Promise を返すパターン

チェーンの中で非同期処理をつなぐ場合、.then() のコールバックから Promise を返す必要がある。Promise を返すと、その Promise が解決されるまで次の .then() は実行されない。

function delay(ms) {
  return new Promise(function(resolve) {
    setTimeout(resolve, ms);
  });
}

delay(1000)
  .then(function() {
    console.log("1秒経過");
    return delay(2000);
  })
  .then(function() {
    console.log("さらに2秒経過");
    return delay(500);
  })
  .then(function() {
    console.log("さらに0.5秒経過");
  });

1 つ目の then で Promise を返す

その Promise が解決されてから次の then が実行される

順番に非同期処理が進む

この「Promise を返すことで次の処理を待たせる」パターンが、Promise チェーンの根幹をなしている。

エラーの伝播と catch の仕組み

Promise チェーンでは、途中のどの段階でエラーが発生しても、チェーンの末尾にある .catch() まで自動的にエラーが伝播する。

getUser(userId)
  .then(function(user) {
    return getOrders(user.id);
  })
  .then(function(orders) {
    // ここでエラーが発生したとする
    throw new Error("注文データの形式が不正です");
  })
  .then(function(detail) {
    // ここはスキップされる
    console.log(detail);
  })
  .catch(function(error) {
    // 上のどの段階のエラーもここで捕捉できる
    console.error("エラー:", error.message);
  });

エラーが発生すると、それ以降の .then() はすべてスキップされ、直近の .catch() にジャンプする。この挙動は同期コードにおける try/catch と似ており、直感的に理解しやすい。

catch の後にチェーンを続ける

.catch() も新しい Promise を返すため、その後にさらに .then() をつなげられる。エラーからの回復処理を書くときに使うパターンだ。

fetchData()
  .then(function(data) {
    return processData(data);
  })
  .catch(function(error) {
    console.warn("エラーが発生、デフォルト値を使用:", error.message);
    return { fallback: true };
  })
  .then(function(result) {
    // 正常時は processData の結果、エラー時は fallback オブジェクトが来る
    console.log("最終結果:", result);
  });

.catch() から値を返すと、次の .then() は fulfilled として実行される。これはエラー回復パターンと呼ばれ、フォールバック値やデフォルト動作を提供する際に便利だ。

エラーが起きても処理を中断せず、代替値で続行する設計手法。

よくある間違い:then の中でネストする

Promise チェーンを使っていても、.then() の中でさらに .then() をネストしてしまうと、コールバック地獄と同じ問題が起きる。

// 悪い例:Promise のネスト
getUser(userId).then(function(user) {
  getOrders(user.id).then(function(orders) {
    getOrderDetail(orders[0].id).then(function(detail) {
      console.log(detail);
    });
  });
});

これでは Promise を使う意味がない。正しくは return で Promise を返してチェーンをフラットにする。

// 良い例:フラットなチェーン
getUser(userId)
  .then(function(user) {
    return getOrders(user.id);
  })
  .then(function(orders) {
    return getOrderDetail(orders[0].id);
  })
  .then(function(detail) {
    console.log(detail);
  });

.then() のコールバックで return を忘れると、次の .then() には undefined が渡される。これも見落としやすいバグの原因となるため注意が必要だ。

Promise チェーンは非同期処理の流れを制御する強力な仕組みだが、チェーンが長くなると依然として読みにくくなることがある。その問題を解消するのが async/await 構文であり、Promise チェーンをさらに同期的な見た目で書けるようにしたものだ。