JavaScript:非同期処理(Promise / async-await)
1学習の目的
- async / await で「待つ処理」を上から下へ読める形で書けるようになる。通信やタイマーが絡んでも処理の順序を制御できるようになる。
- Promise の仕組みとエラー処理を理解し、複数の処理を並列に走らせられるようになる。次章の API 通信はこれなしには書けない。
2基礎解説
| やりたいこと | 書き方 | 備考 |
|---|---|---|
| 結果を待つ | const data = await f(); | async 関数の中でのみ |
| 待てる関数を作る | async function f() { } | 戻り値は必ず Promise |
| 失敗を受け止める | try { await f(); } catch (e) { } | 同期処理と同じ書き方 |
| まとめて並列実行 | await Promise.all([a(), b()]) | 待ち時間が短くなる |
- await を忘れると Promise そのものが入る。表示すると [object Promise] になる。「なぜかデータが取れない」の原因はほぼこれ。
- await は async を付けた関数の中でしか使えない。非同期を呼ぶ側も async にする必要がある。
- async 関数の戻り値は必ず Promise。return 42 と書いても、受け取る側は await が要る。
- エラーは try / catch で受け止める(JS10と同じ)。await を付けないと catch できない点に注意。
- 順番に待つ必要がないなら Promise.all。3秒の処理を3つ直列に待つと9秒だが、並列なら3秒で済む。
現場使用例:APIからのデータ取得、ファイルの読み込み、画像の遅延読み込み、一定時間待ってから再試行。「すぐには終わらない処理」はすべて非同期になる。
// 指定ミリ秒待つ関数(Promiseを返す)
const wait = (ms, value) =>
new Promise((resolve) => setTimeout(() => resolve(value), ms));
// async 関数の中でだけ await が使える
async function main() {
const a = await wait(100, "データA");
console.log(a); // データA
// await を忘れると Promise が入ってしまう
const b = wait(100, "データB");
console.log(b); // Promise { <pending> }
// 失敗は try/catch で受け止める
try {
await Promise.reject(new Error("通信失敗"));
} catch (e) {
console.log(`エラー:${e.message}`);
}
// 順番に待つ必要がなければ並列に
const [x, y] = await Promise.all([wait(100, "X"), wait(100, "Y")]);
console.log(x, y); // X Y(合計100msで完了)
}
main();
3基本ドリル(10問)
出力 == 42
async function getValue() { return 42; } を作り、async function main() の中で await getValue() する。最後に main() を呼ぶ。
async function getValue() {
return 42;
}
async function main() {
const value = await getValue();
console.log(value);
}
main();
async を付けた関数の戻り値は必ず Promise になる。だから 42 をそのまま返しても、受け取る側は await が必要。
A. 結果の値 B. Promise オブジェクト C. undefined D. エラー 選択★☆☆無料
解答 == B
await は「結果が出るまで待って取り出す」役割。
B
表示すると [object Promise] や Promise { <pending> } になる。「なぜかデータが表示されない」の原因はほぼこれ。
出力 == 完了
new Promise((resolve) => setTimeout(() => resolve("完了"), 50)) を返す関数を作る。
const wait = () =>
new Promise((resolve) => setTimeout(() => resolve("完了"), 50));
async function main() {
console.log(await wait());
}
main();
resolve を呼んだ時点で「成功」が確定し、await していた場所に値が渡る。これがPromiseの基本の仕組み。
const getName = async () => "田中";
async function main() {
const name = ____ getName();
console.log(name);
}
main();
出力 == 田中
「待つ」という意味の英単語5文字。
await
これを書かないと Promise { '田中' } のような表示になる。非同期の関数を呼んだら awaitと覚える。
A. どこでも B. async を付けた関数の中 C. try の中だけ D. ループの中だけ 選択★☆☆無料
解答 == B
await と async は必ずセットで使う。
B
async の無い関数で await を書くと SyntaxError になる。非同期を扱う関数は、呼び出す側も連鎖的に async にしていく必要がある。
出力 == エラー:通信失敗
Promise.reject(new Error("通信失敗")) を await し、catch (e) で e.message を出す。
async function main() {
try {
await Promise.reject(new Error("通信失敗"));
} catch (e) {
console.log(`エラー:${e.message}`);
}
}
main();
同期処理とまったく同じ書き方でエラーを扱えるのが async / await の最大の利点。ただし await を付け忘れると catch できないので注意。
const wait = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));
async function main() {
const [a, b] = await Promise.____([wait(20, "A"), wait(20, "B")]);
console.log(a, b);
}
main();
出力 == A B
「すべて」を意味する英単語3文字。
all
直列に await すると待ち時間が足し算になるが、Promise.all なら最も遅いものの時間で済む。20ms × 2 が 20ms になる。
A. 順に await する B. Promise.all でまとめる C. for ループで await D. どれも同じ 選択★★☆無料
解答 == B
互いに依存しない処理は同時に走らせられる。
B
AとCは合計9秒かかるが、Bなら3秒で終わる。ただし「前の結果を次で使う」場合は並列にできないので、順に await するしかない。
出力 == A → B
const a = await wait(...) を2回書く。先に書いたほうが先に完了する。
const wait = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));
async function main() {
const a = await wait(30, "A");
const b = await wait(10, "B");
console.log(`${a} → ${b}`);
}
main();
await は「そこで止まる」。Bのほうが速いのに後になるのは、Aを待ち終わってから初めてBが開始されるため。これが直列実行。
出力 == fulfilled, rejected
allSettled は失敗しても止まらず、各要素の status を教えてくれる。map(r => r.status).join(", ") で取り出す。
async function main() {
const results = await Promise.allSettled([
Promise.resolve("成功"),
Promise.reject(new Error("失敗")),
]);
console.log(results.map((r) => r.status).join(", "));
}
main();
Promise.all は1つでも失敗すると全体が失敗するが、allSettled は全部の結果を最後まで集める。「一部が失敗しても残りは使いたい」場面で使う。
4実践シナリオ(5問)
出力 == ID:1 田中
new Promise の中で setTimeout し、resolve にオブジェクトを渡す。受け取り側は分割代入できる。
const fetchUser = (id) =>
new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: id === 1 ? "田中" : "不明" });
}, 30);
});
async function main() {
const { id, name } = await fetchUser(1);
console.log(`ID:${id} ${name}`);
}
main();
実際のAPI通信もこれと同じ形。次章で学ぶ fetch は、まさにこういう Promise を返す関数になっている。
「await あり:田中」
「await なし:[object Promise]」 コーディング★★☆無料
出力2行が完全一致
await なしの場合はテンプレートリテラルに入れると [object Promise] と表示される。
const getName = async () => "田中";
async function main() {
const withAwait = await getName();
const withoutAwait = getName();
console.log(`await あり:${withAwait}`);
console.log(`await なし:${withoutAwait}`);
}
main();
この [object Promise] を見たら await の付け忘れと即座に判断できるようにしておきたい。画面に出てしまう典型的なバグ。
出力3行が完全一致
Date.now() で開始時刻を記録し、終了時刻との差を測る。直列は await を3回、並列は Promise.all。
const wait = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));
async function main() {
const t1 = Date.now();
const a = await wait(30, "A");
const b = await wait(30, "B");
const c = await wait(30, "C");
const serial = Date.now() - t1;
console.log(`直列:${a},${b},${c}`);
const t2 = Date.now();
const results = await Promise.all([wait(30, "A"), wait(30, "B"), wait(30, "C")]);
const parallel = Date.now() - t2;
console.log(`並列:${results.join(",")}`);
console.log(`並列のほうが速い:${parallel < serial}`);
}
main();
結果は同じでも、かかる時間が3倍違う。直列は約90ms、並列は約30ms。互いに依存しない処理を直列で書くのは、単なる時間の無駄になる。
「[1] 田中」「[2] 取得失敗」「[3] 高橋」「成功2件 / 失敗1件」 コーディング★★★無料
出力4行が完全一致
for...of の中で try/catch する。ループの外に成功・失敗のカウンタを用意する。
const fetchUser = (id) =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (id === 2) {
reject(new Error("取得失敗"));
return;
}
resolve(id === 1 ? "田中" : "高橋");
}, 10);
});
async function main() {
let ok = 0;
let ng = 0;
for (const id of [1, 2, 3]) {
try {
const name = await fetchUser(id);
console.log(`[${id}] ${name}`);
ok++;
} catch (e) {
console.log(`[${id}] ${e.message}`);
ng++;
}
}
console.log(`成功${ok}件 / 失敗${ng}件`);
}
main();
try/catch をループの中に置くのが要点(JS10と同じ)。外に置くと最初の失敗で全体が止まる。reject を呼ぶと catch に飛ぶ。
出力4行。試行1〜3回目と成功(3回目)
let attempt = 0 を外に置き、for ループの中で try/catch する。成功したら return、失敗なら次の周回へ。
let count = 0;
const unstable = () =>
new Promise((resolve, reject) => {
count++;
setTimeout(() => {
if (count < 3) {
reject(new Error("失敗"));
return;
}
resolve("データ");
}, 10);
});
async function retry(maxAttempts) {
for (let i = 1; i <= maxAttempts; i++) {
console.log(`試行${i}回目...`);
try {
await unstable();
console.log(`成功(${i}回目)`);
return;
} catch (e) {
if (i === maxAttempts) {
console.log("すべて失敗しました");
}
}
}
}
retry(3);
通信は一時的に失敗することがあるので、実務では再試行が標準的に組み込まれる。本来は失敗のたびに待ち時間を延ばす(指数バックオフ)が、まずはこの形を押さえたい。
5仕上げ課題
用意する擬似API(すべて Promise を返す):
・fetchProduct(id) … 20ms後に { id, name, price } を返す。id=3 のときは reject(メッセージ「商品が見つかりません」)
データ:1→ノートPC 128000/2→マウス 3200/4→モニター 45000
・fetchStock(id) … 20ms後に在庫数を返す(1→5, 2→0, 4→3)
処理:
① id 1, 2, 3, 4 を順に処理する
② 各IDについて、商品情報と在庫を並列で取得する(Promise.all)
③ 取得できたら「[1] ノートPC:128,000円(在庫5個)」の形式で出力。在庫0なら末尾に「 ※品切れ」を付ける
④ 失敗したら「[3] エラー:商品が見つかりません」と出力し、次へ進む
⑤ 最後に成功件数・失敗件数・取得できた商品の合計金額を出力
出力仕様(6行):
=== 商品情報の取得 ===
[1] ノートPC:128,000円(在庫5個)
[2] マウス:3,200円(在庫0個) ※品切れ
[3] エラー:商品が見つかりません
[4] モニター:45,000円(在庫3個)
成功3件 / 失敗1件 / 合計176,200円
出力6行が完全一致
for...of の中で try/catch し、その中で const [product, stock] = await Promise.all([fetchProduct(id), fetchStock(id)]) とする。品切れの表示は三項演算子で。合計は成功したものだけ足す。
const PRODUCTS = {
1: { name: "ノートPC", price: 128000 },
2: { name: "マウス", price: 3200 },
4: { name: "モニター", price: 45000 },
};
const STOCKS = { 1: 5, 2: 0, 4: 3 };
const fetchProduct = (id) =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (!PRODUCTS[id]) {
reject(new Error("商品が見つかりません"));
return;
}
resolve({ id, ...PRODUCTS[id] });
}, 20);
});
const fetchStock = (id) =>
new Promise((resolve) => {
setTimeout(() => resolve(STOCKS[id] ?? 0), 20);
});
async function main() {
console.log("=== 商品情報の取得 ===");
let ok = 0;
let ng = 0;
let total = 0;
for (const id of [1, 2, 3, 4]) {
try {
const [product, stock] = await Promise.all([fetchProduct(id), fetchStock(id)]);
const mark = stock === 0 ? " ※品切れ" : "";
console.log(`[${id}] ${product.name}:${product.price.toLocaleString()}円(在庫${stock}個)${mark}`);
total += product.price;
ok++;
} catch (e) {
console.log(`[${id}] エラー:${e.message}`);
ng++;
}
}
console.log(`成功${ok}件 / 失敗${ng}件 / 合計${total.toLocaleString()}円`);
}
main();
1件ごとに商品情報と在庫を並列取得しつつ、失敗しても次へ進む——実務のAPI連携そのものの構造になっている。
設計上の要点は2つ。第一に、商品と在庫は互いに依存しないので Promise.all で同時に取る。順に await すると1件あたり40msかかるが、並列なら20msで済む。第二に、try/catch をループの中に置いているので、id=3 で失敗しても id=4 の取得は続く。外に置いていたら、そこで全部止まっていた。
この章で最も大事なのは、await を付けたら「そこで処理が止まる」という感覚だ。止めるべきところでは止め、止める必要がないところは Promise.all で同時に走らせる——その判断ができれば、非同期処理は怖くない。
次章では、この擬似APIを本物のAPIに置き換える。fetch を学べば、世界中のサービスからデータを取得できるようになる。