JavaScript | 非同期処理:エラー処理・例外設計 – Promise 内 throw

JavaScript JavaScript
スポンサーリンク

「Promise 内 throw」を一言でいうと

Promise の中で throw する、というのは
「その Promise を reject(失敗状態)にするための、ちょっと短い書き方」 です。

new Promise((resolve, reject) => {
  if (何かがおかしい) {
    throw new Error("エラーです");
  }
  resolve("成功");
});
JavaScript

これは実は、

new Promise((resolve, reject) => {
  if (何かがおかしい) {
    reject(new Error("エラーです"));
    return;
  }
  resolve("成功");
});
JavaScript

とほぼ同じ意味になります。

ここが重要です。
Promise の中で throw すると、そのエラーは「外側の try / catch」には飛ばず、
「その Promise の reject」として扱われ、.catch(...)awaittry / catch に流れていきます。

この「どこに飛ぶのか」をちゃんとイメージできるかどうかが、Promise 内 throw を理解するカギです。


Promise 内での throw はどう扱われるのか

コンストラクタの中での throw

まずは一番基本の形から。

const p = new Promise((resolve, reject) => {
  console.log("Promise 開始");

  if (true) {
    throw new Error("ここでエラー");
  }

  resolve("成功");
});

p.then((value) => {
  console.log("then:", value);
}).catch((err) => {
  console.log("catch:", err.message);
});
JavaScript

このコードの流れを追ってみます。

  1. new Promise(...) が呼ばれる
  2. すぐに中の関数(executor と呼ばれる)が実行される
  3. throw new Error("ここでエラー") が実行される
  4. JavaScript は「Promise の中で throw された」と判断し、その Promise を reject 状態にする
  5. その結果、then はスキップされ、catch が呼ばれる

実際の出力はこうなります。

Promise 開始
catch: ここでエラー

ここが重要です。
Promise のコンストラクタの中で throw すると、
「その Promise は reject された」とみなされ、
外側の .catch(...) に流れていきます。
外側の同期的な try / catch には届きません。

外側の try / catch では捕まらない例

次のコードを見てください。

try {
  new Promise((resolve, reject) => {
    throw new Error("Promise 内でエラー");
  });
} catch (err) {
  console.log("捕まえた?:", err.message);
}
JavaScript

直感的には「try で囲んでるから捕まるのでは?」と思いがちですが、
この catch は呼ばれません。

なぜかというと、

new Promise(...) の中で起きた throw は、
「その Promise を reject する」という形に変換されるだけで、
同期的な例外として外に飛び出さないからです。

このエラーを扱いたいなら、Promise 側で .catch(...) する必要があります。

new Promise((resolve, reject) => {
  throw new Error("Promise 内でエラー");
})
  .catch((err) => {
    console.log("Promise の catch で捕まえた:", err.message);
  });
JavaScript

ここが重要です。
「Promise 内 throw」は、「Promise の reject になる」のであって、
「外側の try / catch に飛ぶ同期例外」ではありません。
この違いを混同すると、“try / catch 書いたのに捕まらない問題” にハマります。


then の中で throw した場合

then の中の throw も「次の catch に流れる」

Promise コンストラクタの中だけでなく、
then のコールバックの中で throw しても、同じように扱われます。

fetch("https://example.com/api/data")
  .then((response) => {
    if (!response.ok) {
      throw new Error("HTTP エラー: " + response.status);
    }
    return response.json();
  })
  .then((data) => {
    console.log("成功:", data);
  })
  .catch((err) => {
    console.log("catch で捕まえた:", err.message);
  });
JavaScript

ここで起きうるエラーは、例えば次のようなものです。

HTTP ステータスが 200 以外で、自分で throw new Error(...) した
response.json() が失敗して throw した
ネットワークエラーで fetch 自体が reject した

これらはすべて、最後の .catch(...) に流れてきます。

なぜかというと、

then の中で throw すると、
「その then が返す Promise が reject される」
というルールになっているからです。

ここが重要です。
Promise チェーンの中では、
throwreject と同じように、“下流の catch に流すためのシグナル”」として使えます。
throw を使うと、return Promise.reject(...) よりも短く、読みやすく書けることが多いです。


async / await から見た「Promise 内 throw」

await は「reject を throw に変換する」

async / await を使うと、
Promise の reject は「同期例外のように」扱われます。

function getData() {
  return new Promise((resolve, reject) => {
    throw new Error("Promise 内でエラー");
  });
}

async function main() {
  try {
    const data = await getData();
    console.log("成功:", data);
  } catch (err) {
    console.log("async/await の catch:", err.message);
  }
}

main();
JavaScript

ここで起きていることを整理します。

getData() の中で throw
→ Promise が reject 状態になる

await getData()
→ reject された Promise を await すると、その場で throw されたかのように扱われる

その結果、main 関数の try / catchcatch に入ります。

つまり、

Promise 内 throw
→ Promise の reject

await
→ reject を同期例外に変換

try / catch
→ その同期例外を捕まえる

という三段階の変換が起きています。

ここが重要です。
async / await の世界では、
「Promise 内 throw → reject → await で throw → try / catch で捕まえる」
という流れになっている、とイメージしてください。
これが分かると、「Promise 内 throw」と「async / await の try / catch」が頭の中で一本につながります。


reject と throw をどう使い分けるか

reject を直接呼ぶパターン

もちろん、Promise の中で reject を直接呼んでも構いません。

function getUser(id) {
  return new Promise((resolve, reject) => {
    if (!id) {
      reject(new Error("ID が指定されていません"));
      return;
    }

    setTimeout(() => {
      resolve({ id, name: "太郎" });
    }, 1000);
  });
}
JavaScript

この場合、reject を呼んだ時点で Promise は reject 状態になります。

throw を使わずに reject を使うメリットは、
「非同期の中でも明示的に reject できる」ことです。

例えば、setTimeout の中でエラーにしたいときは、
throw ではなく reject を使う必要があります。

new Promise((resolve, reject) => {
  setTimeout(() => {
    // ここで throw しても、Promise の外に飛んでしまう
    // throw new Error("ダメな例");

    // 正しくは reject を呼ぶ
    reject(new Error("タイマー後にエラー"));
  }, 1000);
});
JavaScript

throw を使うのに向いている場所

逆に、throw が向いているのは、
「Promise の executor や then の中で、同期的にエラーを表現したいとき」です。

new Promise((resolve, reject) => {
  if (!url) {
    throw new Error("URL が必要です");
  }
  resolve(url);
});
JavaScript
fetch("/api/data")
  .then((response) => {
    if (!response.ok) {
      throw new Error("HTTP エラー: " + response.status);
    }
    return response.json();
  })
  .catch((err) => {
    console.error("エラー:", err.message);
  });
JavaScript

ここが重要です。
Promise の中で「同期的に」エラーを出したいなら throw
「非同期のコールバックの中で」エラーを出したいなら reject
という使い分けを意識すると、挙動が直感に合いやすくなります。


「Promise 内 throw」で初心者がハマりやすいポイント

外側の try / catch で捕まえようとしてしまう

よくあるパターンがこれです。

try {
  new Promise((resolve, reject) => {
    throw new Error("エラー");
  });
} catch (err) {
  console.log("捕まえたつもり:", err.message);
}
JavaScript

「try で囲んでるのに捕まらない…」となります。

これはもう一度はっきりさせておきたいのですが、

Promise 内 throw
→ Promise の reject
→ 外側の同期的な try / catch には飛ばない

というルールです。

扱いたいなら、Promise 側で .catch(...) するか、
async / await で await してから try / catch する必要があります。

catch を付け忘れて「未処理の Promise 拒否」になる

Promise 内 throw を使っているのに、
最後に .catch(...) を付け忘れると、
ブラウザコンソールに「Unhandled Promise Rejection」的な警告が出ます。

new Promise((resolve, reject) => {
  throw new Error("エラー");
});

// どこにも catch がない
JavaScript

これは、「reject された Promise が、どこにも処理されていない」という状態です。

ここが重要です。
Promise 内 throw を使うときは、
「この Promise は最終的にどこで catch されるのか?」を必ず意識してください。
catch のない Promise 内 throw は、“投げっぱなしのエラー” になります。


初心者として「Promise 内 throw」で本当に押さえてほしいこと

Promise の中で throw すると、その Promise は reject 状態になる。
それは「外側の同期的な try / catch」には届かず、
Promise の .catch(...) や、await を含む try / catch に流れていく。

Promise コンストラクタや then の中での throw は、
return Promise.reject(new Error(...)) とほぼ同じ意味。
ただし、setTimeout など「さらに非同期の中」では throw ではなく reject を使う必要がある。

async / await の世界では、
「Promise 内 throw → reject → await で throw → try / catch で捕まえる」
という流れになっている。

そして一番大事なのは、
「この throw は、最終的にどこで捕まえるつもりなのか?」を常に意識すること。

Promise 内 throw を書いたら、
「この Promise の .catch(...) はどこ?」
「それとも、どこかで await して try / catch している?」
と自分に問いかけてみてください。

その問いを習慣にすると、
「エラーがどこかに消える」「コンソールだけ赤くなる」といった不安定なコードから、
「エラーの流れがちゃんと見える」コードに、一歩ずつ近づいていきます。

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