JavaScript | 非同期エラーの落とし穴(Promise を返さない async と catch の罠)

JavaScript
スポンサーリンク

JavaScript の「非同期エラー」まわりは、初心者が一番つまずくところの1つです。
ここでは、Promiseasync/awaittry/catch の関係と、「Promise を返さない async 関数の罠」について、わかりやすく例を交えて説明します。


まず前提:非同期処理とは?

今すぐ終わらない処理を、後で結果がわかるようにする仕組み」です。
例:

  • API通信 (fetch)
  • ファイル読み込み
  • タイマー (setTimeout)
  • データベース操作

これらはすぐに終わらないので、結果を待たずに次の処理に進む — その結果を後から受け取るのが「Promise」や「async/await」です。


Promise の基本構造

fetch("https://example.com/data.json")
  .then(res => res.json())   // 成功時(Promise が resolve)
  .catch(err => console.error("エラー:", err));  // 失敗時(Promise が reject)
JavaScript

ここで .catch() が付いていれば、通信失敗などの例外を捕まえられます。


async / await の仕組み

async 関数は、必ず Promise を返す関数になります。

async function loadData() {
  const res = await fetch("https://example.com/data.json");
  const data = await res.json();
  return data;
}

// 実は Promise を返している!
const p = loadData();
console.log(p instanceof Promise); // true
JavaScript

なので、呼び出し側では:

loadData()
  .then(data => console.log("OK:", data))
  .catch(err => console.error("NG:", err));
JavaScript

または:

try {
  const data = await loadData();
} catch (e) {
  console.error(e);
}
JavaScript

のように扱います。


よくある落とし穴①:「async関数内でcatchして終わり」パターン

async function badFetch() {
  try {
    const res = await fetch("https://example.com/no-such.json");
    const data = await res.json();
    return data;
  } catch (e) {
    console.error("エラー:", e);
    // ← ここで何も return しない・throw しない
  }
}

badFetch()
  .then(data => console.log("結果:", data))
  .catch(err => console.error("catch:", err));  // ← 実行されない!
JavaScript

🔍 なぜ?

  • async 関数内で catch して 例外を握りつぶす(throw しない) と、
    その Promise は「エラーではなく成功(resolve)」として扱われます。
  • そのため .catch() が呼ばれない!

正しい例(再スローするか、rejectを返す)

async function goodFetch() {
  try {
    const res = await fetch("https://example.com/no-such.json");
    const data = await res.json();
    return data;
  } catch (e) {
    console.error("エラー:", e);
    throw e; // 再スローして、呼び出し側に伝える
  }
}

goodFetch()
  .then(data => console.log("結果:", data))
  .catch(err => console.error("呼び出し側でキャッチ:", err)); // ✅ ここに来る
JavaScript

ポイント:

  • catch の中で throw e すれば、外から見て「Promise が reject された」となる。
  • 「関数の中で握りつぶさず、外に知らせる」ことが重要です。

落とし穴②:「Promise を返さない async 関数」

これも初心者が混乱しやすい。

async function doSomething() {
  try {
    await fetch("https://example.com/data.json");
  } catch (e) {
    console.error("エラー:", e);
  }
}

function start() {
  doSomething(); // ← async を呼んでいるが「return」してない!
}

start()
  .catch(err => console.error("呼び出し側 catch:", err)); // ← 動かない!
JavaScript

🔍 なぜ?

  • start() 自体は普通の関数なので、Promise を返していません。
  • つまり start().catch(...) は意味がない。
  • 中の async 関数のエラーは呼び出し側まで届かない

正しい書き方

  • 呼び出し側に Promise を返すようにする。
function start() {
  return doSomething(); // ← Promise を返す!
}

start()
  .then(() => console.log("完了"))
  .catch(err => console.error("エラーをキャッチ:", err));
JavaScript
  • あるいは「トップレベル」で await する:
async function main() {
  await doSomething();
}

main().catch(console.error);
JavaScript

ポイント:

  • 「上までエラーを伝える」には、Promise を return することが必須。
  • 返していないと .catch() で捕まえられません。

落とし穴③:「非同期エラーをtry/catchで囲っても無効なケース」

try {
  fetch("https://example.com/no-such.json")
    .then(res => res.json());
} catch (e) {
  console.error("これは動かない");
}
JavaScript

🔍 なぜ?

  • try/catch同期的に(その瞬間)実行される処理しか捕まえられません。
  • fetch は Promise を返す非同期処理なので、エラーが起きるのは後。
  • だからこの catch は何も捕まえない。

✅ 対応方法:

  • .catch() を Promise に付けるか
  • await を使って try/catch で囲む。
// OK:await を使う
async function test() {
  try {
    const res = await fetch("https://example.com/no-such.json");
  } catch (e) {
    console.error("エラーを捕まえた:", e);
  }
}
test();
JavaScript

まとめ:非同期エラー処理の落とし穴と対策表

落とし穴原因対策
catch で握りつぶして再throwしない例外が「成功扱い」になるthrow e で再スローする
async関数をreturnしない呼び出し側がPromiseを受け取れないreturn doSomething() する
非同期を同期try/catchで囲むエラー発生タイミングが後.catch() または await + try/catch
then チェーンの中でthrowcatch がないと未処理リジェクト.catch()を最後に必ず付ける
Promiseチェーンの途中return忘れ呼び出し側で結果を受け取れないreturn を忘れない!

練習問題(実践)

問題
次のコードの「バグ」を見つけて、修正してください。
エラーが発生しても 呼び出し側の .catch() に届かない原因は何でしょう?

async function fetchData() {
  try {
    const res = await fetch("https://example.com/404.json");
    return await res.json();
  } catch (e) {
    console.error("内部でキャッチ:", e.message);
  }
}

function run() {
  fetchData(); // ← 問題!
}

run()
  .then(() => console.log("成功"))
  .catch(err => console.error("外でキャッチ:", err)); // ← 動かない
JavaScript

解答:

  • fetchData()return していないので、run() が Promise を返していない。
  • さらに、fetchData 内で例外をキャッチした後 throw e していないため、Promise が「成功扱い」になる。

修正版:

async function fetchData() {
  try {
    const res = await fetch("https://example.com/404.json");
    return await res.json();
  } catch (e) {
    console.error("内部でキャッチ:", e.message);
    throw e; // 再スロー
  }
}

function run() {
  return fetchData(); // Promise を返す
}

run()
  .then(() => console.log("成功"))
  .catch(err => console.error("外でキャッチ:", err)); // ✅ OK
JavaScript

次に、「例外のデバッグ方法(stack の見方)」をやると、これとつながって理解が深まります。

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