目次
async 関数とは何か
async を付けると「戻り値が Promise になる」
function normal() {
return 1;
}
async function asyncFn() {
return 1;
}
console.log(normal()); // 1
console.log(asyncFn()); // Promise { 1 } (fulfilled なPromise)ポイント:
asyncを付けた関数は、どんな値を返しても必ず Promise に“包まれる”- つまり:
async function f() { return 1; } // 実質的には: function f2() { return Promise.resolve(1); }と同じイメージです。
async 関数内の throw は「reject になる」
async function errorFn() {
throw new Error("失敗");
}
const p = errorFn();
console.log(p); // Promise { <rejected> Error: 失敗 }errorFn() を then/catch で扱うこともできます:
errorFn()
.then(() => {
console.log("ここには来ない");
})
.catch(e => {
console.log("catch:", e.message); // "失敗"
});async関数内でthrowしたエラーは、その関数が返す Promise の reject として扱われる- つまり「戻り値もエラーも、全部 Promise によって表現される」
await の基本挙動
await は Promise の「結果」を取り出す
await は、Promise の結果(fulfill 値 or reject エラー)を待つ構文です。
function delay(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
async function main() {
console.log("before");
await delay(1000); // ここで「1秒待つ」
console.log("after"); // 1秒後に実行
}
main();厳密には:
await promiseは、promiseが fulfilled になる → その値がawaitの結果promiseが rejected になる → そこでエラー(例外)が投げられる(=throw相当)
await は「Promise じゃない値」にも使える
async function f() {
const x = await 1;
console.log(x); // 1
}これは内部的に:
await Promise.resolve(1);と同じ扱いです。
awaitは「引数が Promise ならその完了を待つ」「Promise じゃないなら即座に値を返す」- そのため、「どちらが来てもOK」なコードを書きたいときに便利
await はasync 関数内でしか使えない
歴史的に:
awaitはasync functionの中だけ で使える構文- 関数の外(トップレベル)で使うと構文エラー
// NG(非モジュール環境など)
const res = await fetch("/api"); // SyntaxErrorただし:
- ES Modules では、一部の環境で トップレベル await が使えますが、
基本は「async関数の中で使う」と覚えておけばOKです。
async / await で Promise チェーンを書き換える
Promiseチェーンの例
doAsync1()
.then(result1 => doAsync2(result1))
.then(result2 => doAsync3(result2))
.then(result3 => {
console.log("最終結果:", result3);
})
.catch(err => {
console.error("エラー:", err);
});これを async/await に書き換えると:
async function main() {
try {
const result1 = await doAsync1();
const result2 = await doAsync2(result1);
const result3 = await doAsync3(result2);
console.log("最終結果:", result3);
} catch (err) {
console.error("エラー:", err);
}
}
main();- 手続き的な順番で書けるので、直感的に読みやすい
- エラー処理も
try/catchにまとまる
try / catch と async / await
await で reject されたら throw 相当になる
async function main() {
try {
const res = await fetch("/api/data"); // ここでネットワークエラーなど
const json = await res.json();
console.log(json);
} catch (err) {
console.error("非同期エラーをキャッチ:", err);
}
}ここで起きていること:
fetchが Promise を返す- それが reject された場合、
await fetch(...)の行で 例外が投げられたかのように扱われる tryブロック内なので、catchに飛ぶ
関数ごとにエラーハンドリングの「境界」を作る
async 関数は、その中で起きたエラーを 上位の try/catch に伝播 させることができます。
async function fetchUser(id) {
const res = await fetch(`/users/${id}`);
if (!res.ok) {
throw new Error("ユーザー取得失敗: " + res.status);
}
return await res.json();
}
async function main() {
try {
const user = await fetchUser(1);
console.log(user);
} catch (e) {
console.error("アプリ全体としてのエラー処理:", e);
}
}fetchUser内では「ユーザー取得」という責務にフォーカスしてthrowするmainでは「アプリとしてどうするか」を判断する(リトライ・エラーメッセージ表示など)
こうやって、「関数ごと」に エラーの境界 を設計できるのが大きな利点です。
直列処理と並列処理(Promise.all との使い分け)
直列でやる(1つずつ順番に await)
async function fetchSequential() {
const a = await fetch("/api/a");
const b = await fetch("/api/b");
const c = await fetch("/api/c");
console.log(a.status, b.status, c.status);
}fetch("/api/a")が終わってから"/api/b"を呼ぶ → 完全に直列- 各処理が1秒かかるなら、合計約3秒
並列でやる(同時に開始してからまとめて待つ)
async function fetchParallel() {
const pA = fetch("/api/a");
const pB = fetch("/api/b");
const pC = fetch("/api/c");
const [a, b, c] = await Promise.all([pA, pB, pC]);
console.log(a.status, b.status, c.status);
}fetchを先に全部呼んでおいてからPromise.allでまとめて待つ- 3つそれぞれ1秒なら、全体としては「約1秒」で終わるイメージ
配列に対して並列で処理する
async function fetchMany(urls) {
const promises = urls.map(url => fetch(url));
const responses = await Promise.all(promises);
return responses;
}- 各
urlに対してfetchをすぐに呼ぶ → すべて並列で進行 Promise.allで「全部成功するまで待つ」
※ エラー時の挙動(どれか1つでも reject したら Promise.all 全体が reject する)
async / await のよくある落とし穴
forEach と async を組み合わせる罠
// 期待したようには動かない例
async function process(urls) {
urls.forEach(async (url) => {
const res = await fetch(url);
console.log(url, res.status);
});
console.log("end"); // たぶん各fetchより先に出る
}forEach自体は Promise を待たないasyncコールバックの終了を待たないので、console.log("end")が先に実行される
正しいパターン1:for…of で直列処理
async function processSequential(urls) {
for (const url of urls) {
const res = await fetch(url);
console.log(url, res.status);
}
console.log("end"); // 全部終わってから出る
}パターン2:Promise.all で並列処理
async function processParallel(urls) {
const promises = urls.map(async (url) => {
const res = await fetch(url);
console.log(url, res.status);
});
await Promise.all(promises);
console.log("end"); // 全てのfetchが終わってから出る
}async だけ付けて await を書き忘れる
async function main() {
fetch("/api/data"); // await していない
console.log("end");
}- これだと
fetchの完了を待たずにendが出る - その挙動が「意図どおり」ならOKですが、
「待ってから次に進みたい」場合はawaitを忘れないように注意
async 関数からの戻り値をそのまま値として扱ってしまう
async function calc() {
return 1 + 2;
}
const x = calc();
console.log(x); // Promise {...}calc()の戻り値をそのまま数値だと思うとバグる- 必要なら
await calc()か.thenで値を取り出す:
async function main() {
const x = await calc();
console.log(x); // 3
}
コメント