Promise のエラーハンドリング(catch・finally)|JavaScript

Promise を使った非同期処理では、通常の try-catch では同期的なエラーしか捕捉できません。Promise 特有のエラーハンドリング方法を理解しておく必要があります。

catch メソッドの基本

Promise がリジェクト(reject)されると、チェーンの中で最初に見つかった catch に処理が移ります。

fetch('/api/users')
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error('エラー:', error.message);
  });

fetch が失敗したり、JSON のパースに失敗したりすると、catch が呼ばれます。

エラーの伝播

then の中で例外がスローされると、その先の catch で捕捉されます。

Promise.resolve('hello')
  .then(value => {
    throw new Error('then 内でエラー');
  })
  .then(value => {
    console.log('ここは実行されない');
  })
  .catch(error => {
    console.error('キャッチ:', error.message);
  });

エラーが発生すると、その後の then はスキップされて catch に到達します。

複数の catch

長い Promise チェーンでは、途中で catch を挟むこともできます。

fetchUser()
  .then(user => fetchPosts(user.id))
  .catch(error => {
    console.error('ユーザーまたは投稿の取得に失敗');
    return []; // デフォルト値を返して続行
  })
  .then(posts => renderPosts(posts));

catch でエラーを処理した後に値を return すると、次の then に繋がります。

finally メソッド

finally は成功・失敗にかかわらず、最後に必ず実行されます。

showLoadingSpinner();

fetch('/api/data')
  .then(response => response.json())
  .then(data => displayData(data))
  .catch(error => showError(error))
  .finally(() => {
    hideLoadingSpinner(); // 必ず実行される
  });

ローディング表示の解除やリソースのクリーンアップに使います。

catch の位置に注意

catch の位置によって、捕捉できるエラーの範囲が変わります。

チェーンの最後に catch

すべての then で発生したエラーを一括で捕捉する

途中に catch を挟む

その時点までのエラーを処理し、後続の処理を続行できる

// パターン1: 最後で一括処理
promise
  .then(a => process1(a))
  .then(b => process2(b))
  .then(c => process3(c))
  .catch(handleAllErrors);

// パターン2: 途中で回復
promise
  .then(a => process1(a))
  .catch(error => defaultValue)
  .then(b => process2(b))
  .catch(handleError);

Promise.all のエラーハンドリング

Promise.all は、どれか1つでもリジェクトされると全体がリジェクトされます。

Promise.all([
  fetch('/api/users'),
  fetch('/api/posts'),
  fetch('/api/comments')
])
.then(responses => {
  // すべて成功した場合のみここに来る
})
.catch(error => {
  // どれか1つでも失敗するとここに来る
  console.error('いずれかのリクエストが失敗:', error);
});

すべての結果を取得したい場合は Promise.allSettled を使います。

Promise.allSettled([
  fetch('/api/users'),
  fetch('/api/posts'),
  fetch('/api/comments')
])
.then(results => {
  results.forEach((result, i) => {
    if (result.status === 'fulfilled') {
      console.log(`リクエスト${i}: 成功`);
    } else {
      console.log(`リクエスト${i}: 失敗`, result.reason);
    }
  });
});

reject と throw の違い

Promise 内でエラーを発生させるには、reject を呼ぶ方法と throw する方法があります。

// reject を使う
new Promise((resolve, reject) => {
  reject(new Error('明示的にリジェクト'));
});

// throw を使う
new Promise((resolve, reject) => {
  throw new Error('例外をスロー');
});

どちらも結果は同じで、catch で捕捉できます。then の中では throw を使います。

未処理のリジェクションを避ける

catch を付け忘れると「Uncaught (in promise)」警告が出ます。必ずエラー処理を入れましょう。

// 悪い例:catch がない
fetchData();

// 良い例:catch を付ける
fetchData().catch(error => {
  console.error('エラーを処理:', error);
});

未処理のリジェクションはデバッグを困難にし、サイレントな障害を引き起こす可能性があります。