JavaScriptの非同期処理を理解する: Promiseのエラーハンドリング完全ガイドと実践テクニック

はじめに

システム開発第一事業部の奥田です。普段はフロント寄りのフルスタックエンジニアとして、Webアプリの開発を担当しています。

このJavaScriptの非同期処理シリーズでは

と学んできましたがいよいよ最終回となる今回は、エラーハンドリングを徹底的に解説します!

非同期処理を扱う上でエラーハンドリングは避けて通れません。

でも、Promiseのエラー処理は奥が深く、意外と難しいポイントがたくさんあります。

  • 「catchを書いたのにエラーがキャッチできない...」
  • 「Unhandled Promise Rejectionって何?」
  • 「thenの第2引数とcatchって何が違うの?」

こんな経験ありませんか?

この記事ではエラーハンドリングの基礎から実践的なテクニックまでをカバーします。

複雑なPromiseチェーンも自信を持って書けるようになりますよ!

エラーハンドリングの基礎

過去の記事でも触れていますがまずは、Promiseにおけるエラーハンドリングの基本から確認していきましょう!

throwされたエラーは自動的にrejected状態になる

Promise内で例外がthrowされると、自動的にrejected状態になります。

const promise = new Promise((resolve, reject) => {
  throw new Error('例外が発生!');
});

// 内部的には以下と同じ
const promise2 = new Promise((resolve, reject) => {
  try {
    throw new Error('例外が発生!');
  } catch (error) {
    reject(error);
  }
});

Promiseが自動的にtry-catchで囲んでくれているイメージです。 便利ですね!

catchによるエラーのキャッチ

基本的なエラーキャッチは.catch()メソッドで行います。

Promise.resolve('開始')
  .then(() => {
    throw new Error('エラー発生!');
  })
  .catch((error) => {
    console.error('エラーをキャッチ:', error.message);
  });

thenの第2引数 vs catch

実は、.then()メソッドは第2引数にエラーハンドラーを受け取ることができます。

promise.then(
  (result) => {
    // 成功時の処理
    console.log('成功:', result);
  },
  (error) => {
    // 失敗時の処理
    console.log('失敗:', error);
  }
);

過去の記事でも触れましたが、.catch()は内部的に.then(undefined, onRejected)と同じです。

でも、この2つには重要な違いがあります!

それは

  • thenの第2引数は、そのthenより前のエラーしかキャッチできない
  • catchは、そのcatchより前のすべてのエラーをキャッチできる

ということです。

// パターン1: thenの第2引数
promise
  .then(
    (result) => {
      throw new Error('thenの中でエラー');
    },
    (error) => {
      // このエラーハンドラーは上のエラーをキャッチできない!
      console.log('キャッチできない');
    }
  );

// パターン2: catch
promise
  .then((result) => {
    throw new Error('thenの中でエラー');
  })
  .catch((error) => {
    // このcatchは上のエラーをキャッチできる!
    console.log('キャッチできる:', error.message);
  });

この微妙な違い故に、一般的には.catch()を使う方が推奨されます。 (thenの第二引数が取れるというのを知っているのは実にマニアックなことでもあります)

両方設定した場合

じゃあ、.catch().then(undefined, onRejected)を両方設定したらどうなるのか? と疑問に思いませんか?

両方を設定した場合はthenの第2引数が優先されます。

Promise.reject('エラー')
  .then(
    (result) => console.log('成功'),
    (error) => {
      console.log('thenの第2引数:', error);
      // ここで処理されるので、次のcatchには到達しない
    }
  )
  .catch((error) => {
    console.log('catchには来ない');
  });

// 出力: thenの第2引数: エラー

エラーの伝播とスキップ

Promiseチェーンにおけるエラーの動きを詳しく見ていきましょう。

エラーが発生するとthenがスキップされる

エラーが発生すると、次の.catch()までのすべての.then()がスキップされます。

Promise.reject('最初からエラー')
  .then(() => {
    console.log('then1'); // スキップ
  })
  .then(() => {
    console.log('then2'); // スキップ
  })
  .then(() => {
    console.log('then3'); // スキップ
  })
  .catch((error) => {
    console.log('catchでキャッチ:', error);
  });

// 出力:
// catchでキャッチ: 最初からエラー

そして重要なポイントは、catchの後はチェーンが再開されるということです。

Promise.reject('最初からエラー')
  .then(() => {
    console.log('then1'); // スキップ
  })
  .then(() => {
    console.log('then2'); // スキップ
  })
  .then(() => {
    console.log('then3'); // スキップ
  })
  .catch((error) => {
    console.log('catchでキャッチ:', error);
  })
  .then(() => {
    console.log('then4'); // 実行される!
  });

// 出力:
// catchでキャッチ: 最初からエラー
// then4

catchの後のthenがしっかり動いてますね。 エラーから何かを回復させたい時とかにはこういったチェーンの動きを理解している必要がありますね!

途中でエラーが発生した場合

Promiseチェーンの途中でエラーが発生したパターンも復習していきましょう!

以下のコードの流れを予測できますか?

Promise.resolve('開始')
  .then(() => {
    console.log('then1'); // 実行される
    return '次へ';
  })
  .then(() => {
    console.log('then2'); // 実行される
    throw new Error('ここでエラー!');
  })
  .then(() => {
    console.log('then3'); // スキップ
  })
  .then(() => {
    console.log('then4'); // スキップ
  })
  .catch((error) => {
    console.log('catch:', error.message);
    return '復旧';
  })
  .then((result) => {
    console.log('then5:', result); // 実行される
  });

// 出力:
// then1
// then2
// catch: ここでエラー!
// then5: 復旧

catchからのreturn

catchでエラーを処理して値を返すとチェーンは正常な状態として続きます。

Promise.reject('エラー')
  .catch((error) => {
    console.log('エラーを処理:', error);
    return '復旧しました'; // 値を返す
  })
  .then((result) => {
    console.log('thenが実行される:', result);
  })
  .catch((error) => {
    console.log('ここには来ない');
  });

// 出力:
// エラーを処理: エラー
// thenが実行される: 復旧しました

catchの中でPromiseを返す

catchの中でPromiseを返すとそのPromiseの解決を待ってから次のチェーンが実行されます。

Promise.reject('エラー')
  .catch((error) => {
    console.log('エラー処理開始:', error);
    
    // 1秒かけて復旧処理
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve('復旧完了');
      }, 1000);
    });
  })
  .then((result) => {
    console.log('1秒後に実行:', result);
  });

// 出力:
// エラー処理開始: エラー
// (1秒後)
// 1秒後に実行: 復旧完了

catchの中で再びエラーをthrow

catchの中でエラーを再度throwすると、次のcatchに伝播します。

Promise.reject('最初のエラー')
  .catch((error) => {
    console.log('catch1:', error);
    throw new Error('新しいエラー');
  })
  .then(() => {
    console.log('スキップ');
  })
  .catch((error) => {
    console.log('catch2:', error.message);
  });

// 出力:
// catch1: 最初のエラー
// catch2: 新しいエラー

Promise.prototype.finallyの詳細

finallyメソッドは成功・失敗に関わらず実行されますが、いくつか特殊な動作があります。

finallyは値を取らない

Promise.resolve('成功')
  .finally((value) => {
    console.log('finallyの引数:', value); // undefined
  })
  .then((result) => {
    console.log('thenの値:', result); // '成功'
  });

// 出力:
// finallyの引数: undefined
// thenの値: 成功

finallyは引数を受け取らず、常にundefinedになります。

finallyの戻り値は基本的に無視される

finallyの戻り値は基本的には無視されます。

Promise.resolve('元の値')
  .then((v) => {
    console.log('then1:', v);
    return '次の値';
  })
  .finally(() => {
    console.log('finally');
    return 'finallyの戻り値'; // 無視される
  })
  .then((v) => {
    console.log('then2:', v); // '次の値'(finallyの戻り値ではない)
  });

// 出力:
// then1: 元の値
// finally
// then2: 次の値

ただしfinallyでPromiseを返した場合、そのPromiseの解決を待ってから次に進みます。

Promise.resolve('元の値')
  .then((v) => {
    console.log('then1:', v);
    return '次の値';
  })
  .finally(() => {
    console.log('finally開始');
    return new Promise((resolve) => {
      setTimeout(() => {
        console.log('finallyのPromise完了');
        resolve('finallyのPromiseの値'); // この値は無視される
      }, 1000);
    });
  })
  .then((v) => {
    console.log('1秒後のthen:', v); // '次の値'(finallyより前の値)
  });

// 出力:
// then1: 元の値
// finally開始
// (1秒後)
// finallyのPromise完了
// 1秒後のthen: 次の値

Promiseの解決を待つけど、値は無視されるというのがポイントです。

finallyの後にもチェーンは続く

このfinallyですが、finallyの後にもチェーンは続けることができちゃいます。

Promise.resolve('開始')
  .then((v) => {
    console.log('then1:', v);
    return '次';
  })
  .finally(() => {
    console.log('finally1');
  })
  .then((v) => {
    console.log('then2:', v);
    return 'さらに次';
  })
  .finally(() => {
    console.log('finally2');
  })
  .then((v) => {
    console.log('then3:', v);
  });

// 出力:
// then1: 開始
// finally1
// then2: 次
// finally2
// then3: さらに次

finallyの後もチェーンは通常通り続きます。

エラー時のfinally

Promiseでエラーが発生していても、finallyは実行されます。 そしてエラーは次のcatchに伝播します。

Promise.reject('エラー')
  .finally(() => {
    console.log('finallyは実行される');
  })
  .catch((error) => {
    console.log('catchでエラーキャッチ:', error);
  });

// 出力:
// finallyは実行される
// catchでエラーキャッチ: エラー

PromiseIsHandledとUnhandled Promise Rejection

第3回で触れた[[PromiseIsHandled]]についても復習していきましょう!

PromiseIsHandledとは?

[[PromiseIsHandled]]はPromiseの結果が少なくとも一度は処理されたかどうかを示す内部スロットです。

このスロットはboolean値で管理されており、falseの場合は「ハンドリングされていない」とみなされてUnhandled Promise Rejectionが発生します。

// エラーハンドリングなし
const promise1 = Promise.reject('エラー');
// [[PromiseIsHandled]]: false
// → Unhandled Promise Rejection警告が表示される

// エラーハンドリングあり
const promise2 = Promise.reject('エラー');
promise2.catch((e) => console.log(e));
// [[PromiseIsHandled]]: true
// → 警告は表示されない

いつエラーが表示されるのか?

エラーが発生したPromiseに対して、thenもcatchも登録されていない場合にUnhandled Promise Rejectionが発生します。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('1秒後にエラー');
  }, 1000);
});

// 何もハンドリングしない
// → 1秒後にUnhandled Promise Rejection

// 後からcatchを追加してもOK
promise.catch((e) => console.log('エラー処理:', e));

どのPromiseに紐づくかが重要

「 どのPromiseに紐づくか」これが一番難しいポイントでもあります。

理解するためのポイントは以下の通りです。

  • thenは新しいPromiseを返す
  • エラーはエラーが発生したPromiseに紐づく
  • そのPromiseに対してcatchが登録されていないとエラーになる
const promise1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve('成功');
  }, 1000);
});

const promise2 = promise1.then(() => {
  console.log('then1');
  throw new Error('エラー発生!');
});

// promise1にcatchを追加してもpromise2のエラーはキャッチできない
promise1.catch(() => {
  console.log('これはpromise1用'); // 実行されない
});

// promise2のエラーをキャッチするには、promise2にcatchが必要
promise2.catch((error) => {
  console.log('promise2のエラーをキャッチ:', error.message);
});

少し難しいですがエラーハンドリングを理解するためには重要なポイントでもあります!

チェーン内でのエラー処理

チェーン内でのエラー処理も見ていきます。

チェーンの途中でエラーが発生し、catchがない場合は Unhandled Promise Rejectionが発生します。

const promise = Promise.resolve('開始');

promise
  .then(() => {
    console.log('then1');
    throw new Error('エラー!');
  })
  .then(() => {
    console.log('then2'); // スキップ
  })
  .then(() => {
    console.log('then3'); // スキップ
  });
  // ここまでcatchがない → Unhandled Promise Rejection!

解決策としてはcatchでハンドリングすることです。

// 解決策: 最後にcatchを追加
promise
  .then(() => {
    console.log('then1');
    throw new Error('エラー!');
  })
  .then(() => {
    console.log('then2');
  })
  .catch((error) => {
    console.log('エラー処理:', error.message);
  });

分岐したPromiseのエラー

Promiseが分岐しているときはどうでしょうか?

以下のコードで確認してみましょう。

const basePromise = Promise.resolve('開始');

// 分岐1
const branch1 = basePromise.then(() => {
  throw new Error('分岐1でエラー');
});

// 分岐2
const branch2 = basePromise.then(() => {
  console.log('分岐2は正常');
});

// branch1のエラーをキャッチ
branch1.catch((error) => {
  console.log('分岐1のエラー処理:', error.message);
});

// branch2は正常に続く
branch2.then(() => {
  console.log('分岐2は続く');
});

各分岐は独立したPromiseなので、個別にエラーハンドリングが必要になります。

実際の開発での注意点

promiseチェーンを分岐させることは便利ですが、正しく使わないとバグの原因にもなります。

以下のサンプルコードではデータfetchの良い例と悪い例を用意しました。

// ❌ よくある間違い
function fetchData() {
  const promise = fetch('/api/data');
  
  promise.then(response => response.json());
  promise.then(data => console.log(data));
  
  // promiseにcatchがない → エラー時に問題
}

// ✅ 正しい方法
function fetchData() {
  return fetch('/api/data')
    .then(response => response.json())
    .then(data => {
      console.log(data);
      return data;
    })
    .catch(error => {
      console.error('データ取得エラー:', error);
      throw error; // 呼び出し側でも処理できるようにする
    });
}

複雑なPromiseチェーンの解読

実践的な複雑例を見てみましょう。これを理解できれば、Promiseマスターです!

スパゲッティPromiseの例

スパゲッティコードは書かないに越したことはありませんが、実際の開発現場でも遭遇することはよくあります...

少しボリュームがありますが出力順序を予想してみてください!

const basePromise = new Promise((resolve) => {
  setTimeout(() => {
    resolve('成功!');
  }, 1000);
});

// メインチェーン
basePromise
  .then((v) => {
    console.log('then1:', v);
    return '次のthen2へ';
  })
  .finally(() => console.log('finally1'))
  .then((v) => {
    console.log('then2:', v);
    throw new Error('エラー発生');
  })
  .then((v) => {
    console.log('then3:', v); // スキップ
  })
  .catch((e) => {
    console.log('catch1:', e.message);
    return '次の処理';
  })
  .finally(() => console.log('finally2'))
  .finally(() => {
    console.log('finally3');
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve('1秒後の処理');
      }, 1000);
    });
  })
  .then((v) => {
    console.log('then4:', v);
  });

// 分岐1
basePromise
  .then((v) => {
    console.log('branch1-then1:', v);
    throw new Error('分岐1エラー');
  });
  // catchがない → エラー!

// 分岐2
basePromise.then((v) => {
  console.log('branch2-then1:', v);
});

// 分岐3
const branch3 = basePromise
  .then((v) => {
    console.log('branch3-then1:', v);
    return '分岐3の値';
  })
  .catch((e) => {
    console.log('branch3-catch:', e);
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve('1秒後の処理');
      }, 1000);
    });
  })
  .finally(() => console.log('branch3-finally'));

branch3.then((v) => {
  console.log('branch3-then2:', v);
});

答えを見る(クリック)

// 1秒後(basePromiseが解決)
then1: 成功!
branch1-then1: 成功!
branch2-then1: 成功!
branch3-then1: 成功!

finally1
then2: 次のthen2へ
catch1: エラー発生
finally2
finally3
branch3-finally
branch3-then2: 分岐3の値

// さらに1秒後(finally3のPromiseが解決)
then4: 次の処理

// エラー
Unhandled Promise Rejection: Error: 分岐1エラー

ポイント解説

  1. basePromiseは1秒後に解決される
  2. basePromiseから派生した全てのthenが同時に実行される
  3. メインチェーンでエラーが発生し、catch1でキャッチ
  4. finally3でPromiseを返すため、1秒待つ
  5. 分岐1はcatchがないため、Unhandled Promise Rejection

async/awaitとの比較

現代のJavaScriptではasync/awaitがよく使われます。

async/awaitPromiseを同期的に書けるようにした糖衣構文です。

Promiseとの違いを理解しましょう。

async関数は必ずPromiseを返す

async関数は必ずPromiseを返します。

async function getData() {
  return '結果';
}

// これは以下と同じ
function getData() {
  return Promise.resolve('結果');
}

// 使い方
getData().then((result) => {
  console.log(result); // '結果'
});

awaitで待つ

awaitを使うことで、Promiseの完了を待って結果を受け取ることができます。

// Promise版
function fetchUserData() {
  return fetchUser()
    .then((user) => {
      return fetchPosts(user.id);
    })
    .then((posts) => {
      return fetchComments(posts[0].id);
    })
    .then((comments) => {
      return comments;
    });
}

// async/await版
async function fetchUserData() {
  const user = await fetchUser();
  const posts = await fetchPosts(user.id);
  const comments = await fetchComments(posts[0].id);
  return comments;
}

async/awaitの方が読みやすいですね!

エラーハンドリングの違い

Promiseとasync/awaitにおけるエラーハンドリングの違いを見てみましょう。

Promise版:

fetchData()
  .then((data) => {
    console.log('成功:', data);
    return processData(data);
  })
  .then((result) => {
    console.log('処理完了:', result);
  })
  .catch((error) => {
    console.error('エラー:', error);
  })
  .finally(() => {
    console.log('終了');
  });

async/await版:

async function handleData() {
  try {
    const data = await fetchData();
    console.log('成功:', data);
    
    const result = await processData(data);
    console.log('処理完了:', result);
  } catch (error) {
    console.error('エラー:', error);
  } finally {
    console.log('終了');
  }
}

handleData();

大きな特徴としてasync/awaitでは慣れ親しんだtry-catchが使えます!

どちらを使うべきか?

ではPromiseとasync/awaitどちらを使うべきなのでしょうか?

迷いどころですが両者の特徴は以下の通りです。

async/awaitが向いている場合:

  • ✅ 順次処理が多い
  • ✅ エラーハンドリングをまとめたい
  • ✅ コードの可読性を重視

Promiseが向いている場合:

  • ✅ 並列処理が主体
  • ✅ チェーンが複雑
  • ✅ 細かいエラーハンドリングが必要

実際は両方を組み合わせて使うのがベストです!

その時々の状況において使い分けができれば一流のエンジニアですね。

雰囲気としては以下のような感じです。

async function complexProcess() {
  try {
    // 並列処理はPromise.all
    const [user, config] = await Promise.all([
      fetchUser(),
      fetchConfig()
    ]);
    
    // 順次処理はawait
    const posts = await fetchPosts(user.id);
    const enrichedPosts = await enrichPosts(posts, config);
    
    return enrichedPosts;
  } catch (error) {
    console.error('エラー:', error);
    throw error;
  }
}

よくあるアンチパターン

では実際の開発で見かける避けるべきパターンをいくつか紹介します。

❌ アンチパターン1: Promise内でPromiseをネスト

// ❌ 悪い例
function badExample() {
  return fetchUser().then((user) => {
    return fetchPosts(user.id).then((posts) => {
      return fetchComments(posts[0].id).then((comments) => {
        return comments;
      });
    });
  });
}

// ✅ 良い例
function goodExample() {
  return fetchUser()
    .then((user) => fetchPosts(user.id))
    .then((posts) => fetchComments(posts[0].id))
    .then((comments) => comments);
}

Promiseでコールバック地獄を作らないように!

❌ アンチパターン2: catchなしのPromise

// ❌ 悪い例
function badExample() {
  fetchData().then((data) => {
    processData(data);
  });
  // エラーが発生したらどうする?
}

// ✅ 良い例
function goodExample() {
  fetchData()
    .then((data) => processData(data))
    .catch((error) => {
      console.error('エラー処理:', error);
      // 適切なエラーハンドリング
    });
}

必ずエラーハンドリングを追加しましょう!

❌ アンチパターン3: 不要なnew Promise()ラッピング

// ❌ 悪い例
function badExample() {
  return new Promise((resolve, reject) => {
    fetchData()
      .then((data) => resolve(data))
      .catch((error) => reject(error));
  });
}

// ✅ 良い例
function goodExample() {
  return fetchData(); // すでにPromiseを返している
}

既にPromiseを返す関数を、さらにPromiseで囲む必要はありません。

❌ アンチパターン4: Promiseを返すのを忘れる

// ❌ 悪い例
function badExample() {
  fetchUser().then((user) => {
    fetchPosts(user.id); // returnを忘れている!
  })
  .then((posts) => {
    // postsはundefined
    console.log(posts);
  });
}

// ✅ 良い例
function goodExample() {
  return fetchUser()
    .then((user) => {
      return fetchPosts(user.id); // returnする
    })
    .then((posts) => {
      console.log(posts); // 正しい値
    });
}

thenの中でPromiseを返すときは、必ずreturnしましょう!

❌ アンチパターン5: async関数内でthenを使う

// ❌ 悪い例
async function badExample() {
  return fetchData().then((data) => {
    return processData(data);
  });
}

// ✅ 良い例
async function goodExample() {
  const data = await fetchData();
  return processData(data);
}

async/awaitを使うなら、thenは使わない方がシンプルです。

❌ アンチパターン6: forEach内でawait

// ❌ 悪い例(順次処理されない)
async function badExample(ids) {
  ids.forEach(async (id) => {
    const data = await fetchData(id); // 並列実行される
    console.log(data);
  });
}

// ✅ 良い例1(順次処理)
async function goodExample1(ids) {
  for (const id of ids) {
    const data = await fetchData(id);
    console.log(data);
  }
}

// ✅ 良い例2(並列処理)
async function goodExample2(ids) {
  const promises = ids.map(id => fetchData(id));
  const results = await Promise.all(promises);
  results.forEach(data => console.log(data));
}

forEachはawaitを待ってくれません!

実践的なパターン

実務で使える便利なパターンもいくつか紹介します。

パターン1: リトライ処理

APIリクエストが失敗したときに、自動的にリトライする処理です。

function retry(fn, maxAttempts = 3, delay = 1000) {
  return Promise.resolve()
    .then(fn) // 同期throwも拾う
    .catch((error) => {
      if (maxAttempts <= 1) {
        throw error;
      }

      console.log(`リトライします... 残り${maxAttempts - 1}回`);

      // 指定時間待ってからリトライ(簡易バックオフ)
      return new Promise((resolve) => setTimeout(resolve, delay)).then(() =>
        retry(fn, maxAttempts - 1, delay * 2)
      );
    });
}

// 使用例
retry(() => fetch('/api/unstable-endpoint'), 3, 1000)
  .then((response) => response.json())
  .then((data) => console.log('成功:', data))
  .catch((error) => console.error('全てのリトライが失敗:', error));

パターン2: タイムアウト処理

処理に時間制限を設ける方法です。

// AbortControllerで実際に中断する版
function timeoutFetch(input, ms, init) {
  const controller = new AbortController();
  const timer = setTimeout(() => {
    controller.abort(new Error(`タイムアウト: ${ms}ms経過`));
  }, ms);

  return fetch(input, { ...init, signal: controller.signal })
    .finally(() => clearTimeout(timer));
}

// 使用例
timeoutFetch('/api/slow-endpoint', 3000)
  .then((response) => response.json())
  .then((data) => console.log('成功:', data))
  .catch((error) => {
    if (error.name === 'AbortError') {
      console.error('リクエストがタイムアウトしました');
    } else {
      console.error('エラー:', error);
    }
  });

パターン3: 順次実行

配列の要素を順番に処理する方法です。

// 失敗した時点で止まる版
function sequential(tasks) {
  return tasks.reduce((promise, task) => {
    return promise.then((results) => {
      return Promise.resolve()
        .then(task)
        .then((result) => [...results, result]);
    });
  }, Promise.resolve([]));
}

// 全件の成功・失敗を集計する版
async function sequentialAllSettled(tasks) {
  const results = [];
  for (const task of tasks) {
    try {
      results.push({ status: 'fulfilled', value: await task() });
    } catch (error) {
      results.push({ status: 'rejected', reason: error });
    }
  }
  return results;
}

// 使用例
const tasks = [
  () => fetch('/api/data1').then(r => r.json()),
  () => fetch('/api/data2').then(r => r.json()),
  () => fetch('/api/data3').then(r => r.json())
];

sequential(tasks).then((results) => {
  console.log('全ての結果:', results);
});

sequentialAllSettled(tasks).then((results) => {
  console.log('成功/失敗を含めた結果:', results);
});

パターン4: 並列実行(制限付き)

同時実行数を制限しながら並列処理する方法です。

async function parallelLimit(tasks, limit) {
  if (limit <= 0) {
    throw new Error('limitは1以上にしてください');
  }

  const results = [];
  const executing = [];

  for (const [index, task] of tasks.entries()) {
    const promise = Promise.resolve().then(() => task());
    results[index] = promise;

    const wrapped = promise.finally(() => {
      const i = executing.indexOf(wrapped);
      if (i >= 0) executing.splice(i, 1);
    });
    executing.push(wrapped);

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

  return Promise.all(results);
}

// 使用例:最大3つまで同時実行
const tasks = Array.from({ length: 10 }, (_, i) => {
  return () => {
    console.log(`タスク${i}開始`);
    return new Promise((resolve) => {
      setTimeout(() => {
        console.log(`タスク${i}完了`);
        resolve(i);
      }, Math.random() * 2000);
    });
  };
});

parallelLimit(tasks, 3)
  .then((results) => {
    console.log('全て完了:', results);
  });

パターン5: エラーからの復旧

エラーが発生しても、デフォルト値で続行する方法です。

function withFallback(promise, fallbackValue) {
  return promise.catch((error) => {
    console.warn('エラーが発生しましたが、デフォルト値で続行:', error);
    return fallbackValue;
  });
}

// 使用例
Promise.all([
  withFallback(fetch('/api/user').then(r => r.json()), { name: 'Guest' }),
  withFallback(fetch('/api/settings').then(r => r.json()), { theme: 'default' }),
  withFallback(fetch('/api/notifications').then(r => r.json()), [])
])
  .then(([user, settings, notifications]) => {
    // いずれかが失敗してもデフォルト値で処理を続行
    console.log(user, settings, notifications);
  });

パターン6: Promise.allSettledでの個別処理

複数の非同期処理(主に複数API呼び出し)を並列で投げて、成功と失敗を“分けて扱いたい”場面で使います。

async function fetchMultipleWithHandling(urls) {
  const promises = urls.map(url => 
    fetch(url).then(r => r.json())
  );
  
  const results = await Promise.allSettled(promises);
  
  const successful = [];
  const failed = [];
  
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      successful.push({ url: urls[index], data: result.value });
    } else {
      failed.push({ url: urls[index], error: result.reason });
    }
  });
  
  console.log(`成功: ${successful.length}件, 失敗: ${failed.length}件`);
  
  return { successful, failed };
}

// 使用例
fetchMultipleWithHandling([
  '/api/data1',
  '/api/data2',
  '/api/data3'
])
  .then(({ successful, failed }) => {
    successful.forEach(({ url, data }) => {
      console.log(`${url} の取得に成功:`, data);
    });
    
    failed.forEach(({ url, error }) => {
      console.error(`${url} の取得に失敗:`, error);
    });
  });

デバッグのコツ

Promiseのデバッグは難しいことがあります。

なので、非同期処理のデバッグで役立つテクニックを紹介していきます!

1. 各段階でログを出す

「各段階でログを出す」と良い理由はPromiseチェーンは途中で値が変わったり、どこかでthrowされると後続の.then()がスキップされたりして、“どこまで進んだか/どこで壊れたか”が見えにくいからです。

ログを段階ごとに置くと次が一気に楽になります。

fetchData()
  .then((data) => {
    console.log('1. データ取得成功:', data);
    return processData(data);
  })
  .then((result) => {
    console.log('2. 処理完了:', result);
    return saveData(result);
  })
  .then((saved) => {
    console.log('3. 保存完了:', saved);
  })
  .catch((error) => {
    console.error('エラー発生:', error);
    console.error('スタックトレース:', error.stack);
  });

2. Promise.allSettledでエラーを個別に確認

Promise.allSettled()を使う理由は「複数のPromiseのうち失敗したやつだけを特定して状況を確認したい」からです。

Promise.allSettled()は全件分の結果を必ず返すので、fulfilled / rejectedを1件ずつ見て「どれが落ちたか」「理由(reason)は何か」を個別にログできるのがメリットです。

const results = await Promise.allSettled([
  promise1,
  promise2,
  promise3
]);

results.forEach((result, index) => {
  console.log(`Promise ${index}:`, result);
});

3. async/awaitでスタックトレースを見やすく

async/awaitを使う理由は「エラーが起きたときに どの処理の流れで落ちたか を追いやすくするため」です。

以下のこんなメリットがあります。

  • awaitは見た目が同期処理に近いので、fetchUser()→fetchPosts()→fetchComments()みたいな“一本道”の流れを人間が読み取りやすい
  • try-catchでエラー処理を1か所にまとめられて、「どこで例外になったか」を追いやすい(thenチェーンより分岐が減る)
  • Promiseチェーンは.then()のコールバックをまたぐのでデバッガで追うときに呼び出し関係が分かれやすく、結果としてスタックトレース(やエラー原因の追跡)が難しく感じやすい
// Promise版(スタックトレースが追いにくい)
function promiseVersion() {
  return fetchUser()
    .then(user => fetchPosts(user.id))
    .then(posts => fetchComments(posts[0].id));
}

// async/await版(スタックトレースが分かりやすい)
async function asyncVersion() {
  const user = await fetchUser();
  const posts = await fetchPosts(user.id);
  const comments = await fetchComments(posts[0].id);
  return comments;
}

4. Unhandled Promise Rejectionの監視

.catch()などで処理されなかったPromiseの失敗(Unhandled Promise Rejection)をアプリ全体で拾ってログ出し・通知・調査できるようにするためです。

// グローバルなエラーハンドラーを設定
window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled Promise Rejection:', event.reason);
  // エラーロギングサービスに送信、など
});

// Node.jsの場合
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Promise Rejection at:', promise, 'reason:', reason);
});

まとめ

4回に渡るPromiseシリーズ、お疲れさまでした!

最終回ではエラーハンドリングを中心に実践的な内容を解説しました。

実戦で使えるポイントを盛り込んだのどんどん実践で活かしていってください!

今回学んだこと

  • エラーハンドリングの基礎:thenの第2引数 vs catch
  • エラーの伝播:エラー発生時のスキップとチェーンの再開
  • finallyの詳細:戻り値は無視されるが、Promiseは待つ
  • PromiseIsHandled:未処理の拒否を検出する仕組み
  • 複雑なチェーン:どのPromiseに紐づくかを意識する
  • async/awaitとの比較:それぞれの使いどころ
  • アンチパターン:避けるべき書き方
  • 実践的なパターン:リトライ、タイムアウト、並列処理など
  • デバッグのコツ:効率的なデバッグ方法

シリーズ全体の振り返り

第1回 では、コールバックからPromiseへの進化を学びました。
第2回 では、Promiseの基本と使い方を学びました。
第3回 では、内部構造とイベントループを理解しました。
第4回 では、エラーハンドリングと実践テクニックをマスターしました。

これであなたもPromiseマスターです!🎉

今後の学習

Promiseを理解したら、次のステップとして以下もおすすめです:

async/awaitをより深く学ぶ
Fetch APIを使った実践的なHTTP通信
RxJSなどのリアクティブプログラミング
Web Workersでの並列処理
Service Workersでのオフライン対応

実践で活かそう

学んだ知識は、実際のコードで使ってこそ身につきます。

以下を試してみてください:

  • 既存のコードをPromiseでリファクタリング
  • エラーハンドリングを見直す
  • 実践的なパターンを自分のプロジェクトに適用

参考リンク

Promiseについてさらに深く学びたい方へ:


最後まで読んでいただき、本当にありがとうございました!
このシリーズがあなたのJavaScript学習の助けになれば嬉しいです。


テコテックの採用活動について

テコテックでは新卒・中途採用を積極的に行っています。 採用サイトでは会社の雰囲気や福利厚生、募集ポジションをご確認いただけます。 ご興味をお持ちいただけましたら、ぜひチェックしてみてください。 tecotec.co.jp