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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?