JavaScript の「非同期エラー」まわりは、初心者が一番つまずくところの1つです。
ここでは、Promise・async/await・try/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 チェーンの中でthrow | catch がないと未処理リジェクト | .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 の見方)」をやると、これとつながって理解が深まります。
