- JavaScript
- 技術 💻
JavaScriptのasync/awaitの使い方|エラー処理と並列実行
async/awaitは、JavaScriptの非同期処理(Promise)を、普通の処理と同じように「上から順に」読める形で書くための構文です。関数の前に async を付け、結果を待ちたい処理の前に await を付けると、その処理が終わるまで次の行に進みません。
書き方はシンプルですが、エラー処理を忘れる、同時に実行できる処理を1つずつ待ってしまう、forEachの中でawaitしても待ってくれないといった落とし穴があります。この記事では、基本の書き方から、実務でつまずきやすいポイントまでを、Node.jsで実際に実行した結果とあわせて解説します。
非同期処理とは
JavaScriptは、時間のかかる処理(通信、ファイルの読み込み、タイマーなど)の完了を待たずに、次の処理に進みます。これを非同期処理と呼びます。待っている間も画面の操作が止まらないのは、この仕組みのおかげです。
console.log("1");
setTimeout(() => console.log("2"), 1000);
console.log("3");
// 表示される順番:1 → 3 → 2
非同期処理の結果を受け取る仕組みが Promise で、Promiseを読みやすく書くための構文が async/await です。
基本の書き方
async function getUser(id) {
const res = await fetch(`https://api.example.com/users/${id}`);
const user = await res.json();
return user;
}
const user = await getUser(1);
console.log(user.name);
| 構文 | 意味 |
|---|---|
async function |
この関数は非同期処理を含む。戻り値は必ずPromiseになる |
await Promise |
Promiseの結果が出るまで待ち、結果の値を取り出す |
async 関数の中で return user と書いても、呼び出し側が受け取るのはuserを包んだPromiseです。そのため、呼び出す側でも await が必要になります。
async function f() { return 1; }
console.log(f() instanceof Promise); // true
thenとの比較
// then で書いた場合
fetch("/api/users/1")
.then((res) => res.json())
.then((user) => console.log(user.name))
.catch((err) => console.error(err));
// async/await で書いた場合
try {
const res = await fetch("/api/users/1");
const user = await res.json();
console.log(user.name);
} catch (err) {
console.error(err);
}
処理が増えるほど、async/awaitの方が上から順に読めるため、流れを追いやすくなります。
awaitを使える場所
asyncを付けた関数の中- ESモジュールのトップレベル(
<script type="module">、Node.jsの.mjsや"type": "module"の設定時)
通常の関数の中で await を使うと、構文エラーになります。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JavaScriptのasync/awaitの使い方|エラー処理と並列実行」
エラー処理:try/catch
await したPromiseが失敗(reject)すると、その場で例外が発生します。try/catch で受け止めます。
async function loadUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`); // 404や500はここで例外にする
}
return await res.json();
} catch (err) {
console.error("ユーザーの取得に失敗:", err.message);
return null;
}
}
fetch は、404や500が返ってきても失敗(reject)になりません。通信自体が成功していれば、正常に結果を返します。res.ok(ステータスが200番台か)を確認して、自分で例外にする必要があります。
エラーを受け止め忘れると
await も catch もせずに、失敗するPromiseを放置すると、エラーがどこにも受け止められません。
function noAwait() {
try {
failAsync(); // awaitしていないので、tryで受け止められない
} catch (e) {
console.log("ここには来ない");
}
}
実際に実行すると、catchには入らず、「処理されなかったPromiseのエラー(unhandledRejection)」として扱われました。Node.jsでは、このエラーでプロセスが終了します。非同期の処理を呼ぶときは、awaitするか、.catch() を付けるのが原則です。
順番に待つか、同時に実行するか
1つずつ待つと、時間が足し算になる
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
await sleep(300);
await sleep(300);
await sleep(300);
// 実測:約900ミリ秒
前の結果を使わない、互いに独立した処理を1つずつ待つと、待ち時間がそのまま足し算になります。
Promise.allで同時に実行する
await Promise.all([sleep(300), sleep(300), sleep(300)]);
// 実測:約300ミリ秒
Promise.all は、すべての処理を同時に開始し、全部が終わるまで待ちます。独立した3つのAPIを呼ぶなら、これだけで待ち時間が3分の1になります。
const [user, orders, points] = await Promise.all([
fetchUser(id),
fetchOrders(id),
fetchPoints(id),
]);
使い分け
| 方法 | 動き | 1つが失敗したら | 使う場面 |
|---|---|---|---|
順番に await |
1つずつ実行 | そこで止まる | 前の結果を次で使う |
Promise.all |
同時に実行、全部を待つ | すぐに全体が失敗 | 全部そろわないと意味がない |
Promise.allSettled |
同時に実行、全部を待つ | 他は続行し、成否を個別に返す | 一部が失敗しても、成功した分は使いたい |
Promise.race |
同時に実行、最初の1つを待つ | 最初に終わったものが失敗なら失敗 | タイムアウトの実装など |
Promise.any |
同時に実行、最初の成功を待つ | 全部失敗したら失敗 | 複数の取得先のどれかから取れればよい |
const results = await Promise.allSettled([fetchA(), fetchB()]);
// [{status: "fulfilled", value: ...}, {status: "rejected", reason: Error}]
落とし穴:forEachの中でawaitしても待たない
実務で最も多い間違いの1つです。
const ids = [1, 2, 3];
const results = [];
ids.forEach(async (id) => {
await sleep(100);
results.push(id);
});
console.log(results.length); // 実測:0(まだ1件も終わっていない)
forEach は、渡された関数が返すPromiseを待たずに、次の要素へ進み、すぐに終わります。そのため、forEachの直後では、どの処理もまだ完了していません。
順番に処理したい場合:for...of
for (const id of ids) {
await sleep(100);
results.push(id);
}
// 実測:3件、約300ミリ秒(1つずつ順番に)
同時に処理したい場合:map + Promise.all
const results = await Promise.all(
ids.map(async (id) => {
await sleep(100);
return id * 2;
})
);
// 実測:[2, 4, 6]、約100ミリ秒(同時に)
| 書き方 | 待つか | 実行の順番 |
|---|---|---|
forEach(async ...) |
待たない | ― |
for...of + await |
待つ | 1つずつ順番に |
map + Promise.all |
待つ | 同時に |
map・filter・reduce なども、中の関数のPromiseを待ちません。配列の要素ごとに非同期処理をするときは、for...of か Promise.allと覚えておきます。
同時に実行しすぎに注意
数千件のAPI呼び出しを Promise.all で一度に実行すると、相手のサーバーに負荷をかけたり、回数制限(429エラー)に引っかかったりします。大量の場合は、10件ずつなど同時実行数を制限します(p-limitなどのライブラリも使えます)。
タイムアウトを付ける
fetch は、既定では応答が返ってくるまで待ち続けます。AbortSignal.timeout() で上限を付けられます。
try {
const res = await fetch(url, { signal: AbortSignal.timeout(5000) }); // 5秒
} catch (err) {
if (err.name === "TimeoutError") {
console.error("タイムアウトしました");
}
}
実際に、応答に1秒かかるサーバーへ200ミリ秒のタイムアウトで接続すると、TimeoutError になりました。
画面の操作でリクエストを中断したい場合(検索の入力が変わった、画面を離れたなど)は、AbortController を使います。
const controller = new AbortController();
fetch(url, { signal: controller.signal });
controller.abort(); // 中断する
よくある間違いのまとめ
| 間違い | 起きること | 正しい書き方 |
|---|---|---|
await を付け忘れる |
値ではなくPromiseが変数に入る([object Promise]) |
await を付ける |
res.ok を確認しない |
404や500でも成功として処理が進む | if (!res.ok) throw ... |
| 独立した処理を1つずつ待つ | 待ち時間が足し算になる | Promise.all |
forEach の中で await |
処理の完了を待たない | for...of か Promise.all |
| awaitもcatchもしない | エラーが受け止められず、Node.jsではプロセスが落ちる | await するか .catch() |
| タイムアウトがない | 応答がないと、いつまでも待つ | AbortSignal.timeout() |
よくある質問
Q. async/awaitを使うと、処理は同期(順番どおり)になるのですか?
A. 見た目は順番どおりですが、中身は非同期のままです。await している間、JavaScriptは他の処理(画面の操作やタイマーなど)を進めています。その関数の中の処理が、順番に進むように書けるだけです。
Q. awaitはどこでも使えますか? A. async関数の中と、ESモジュールのトップレベルで使えます。それ以外の場所ではエラーになります。
Q. thenとasync/awaitはどちらを使うべきですか? A. 新しいコードでは、読みやすさからasync/awaitが主流です。どちらもPromiseを扱う書き方なので、混在させても動作します。
Q. Reactの useEffect の中で async を使いたいです。 A. useEffectに渡す関数そのものは async にできないため、中でasync関数を定義して呼び出します。詳しくは、useEffectの使い方の記事で解説しています。
まとめ
- async/awaitは、Promiseを上から順に読める形で書く構文。async関数は必ずPromiseを返す
- エラーは try/catch で受け止める。fetchは404・500でもrejectしないので
res.okを確認する - 独立した処理は Promise.all で同時に実行する(実測:900ミリ秒 → 300ミリ秒)
- forEachの中のawaitは待たない。順番なら for...of、同時なら map+Promise.all
- 大量の同時実行は制限し、通信には タイムアウト を付ける
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JavaScriptのasync/awaitの使い方|エラー処理と並列実行」
株式会社RJCでは、AI駆動開発や個人開発の場面でも、生成されたコードの動きを理解して確かめる力を大切にしています。こうした知識を仲間と共有しながら、一緒に成長を楽しんでいます。
株式会社RJC ― SI事業・SES事業・AI駆動開発。未経験からエンジニアに育てるのが得意な会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。