Promiseとasync/awaitの違いと使い分け|JavaScriptの非同期処理をNode.jsで動かして解説

async/awaitはPromiseを置き換えるものではなく、Promiseを読みやすく書くための構文です。async関数は必ずPromiseを返し、awaitはPromiseの結果が出るまでその関数の続きを待ちます。順番に進む処理はasync/awaitで書き、複数の処理を同時に走らせるときはPromise.allなどPromiseのメソッドを組み合わせる、という使い分けが基本です。

この記事のコードはすべて、2026年10月14日にNode.js v24.21.0(最新のLTS)で実行し、表示された結果をそのまま載せています。2026年10月14日時点の最新リリースはv26.11.1です(Node.js Releases)。ファイルは拡張子を.mjsにして、ES Modulesとして実行しています。

PromiseとPromiseの3つの状態

Promiseは、非同期処理の「いずれ決まる結果」を表すオブジェクトです。状態は次の3つのいずれかで、一度決まった結果は変わりません(MDN Promise)。

状態 意味 受け取り方
pending(待機) まだ結果が出ていない –
fulfilled(履行) 成功して値が決まった .then() または await の戻り値
rejected(拒否) 失敗して理由(エラー)が決まった .catch() または try/catch

async/awaitは2017年版のECMAScript(ES2017)で標準になりました(TC39 Finished Proposals)。async関数の戻り値は常にPromiseで、関数の中でreturnした値はfulfilledの値に、throwしたエラーはrejectedの理由になります(MDN async function)。

同じ処理をPromiseチェーンとasync/awaitで書く

100ミリ秒後にユーザー情報を返す関数を用意し、2回続けて呼び出す処理を2通りで書きます。async/await側では、2回目にわざと不正なIDを渡してエラーを起こしています。

// 指定ミリ秒後にユーザー情報を返す「通信のふり」をする関数
function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id <= 0) reject(new Error(`不正なID: ${id}`));
      else resolve({ id, name: `user${id}` });
    }, 100);
  });
}

// Promiseチェーンで書く
fetchUser(1)
  .then((user) => {
    console.log("then:", user.name);
    return fetchUser(2);
  })
  .then((user) => console.log("then:", user.name))
  .catch((err) => console.error("then側のエラー:", err.message));

// async/awaitで書く(中身は同じPromise)
async function main() {
  try {
    const a = await fetchUser(1);
    console.log("await:", a.name);
    const b = await fetchUser(0);
    console.log("await:", b.name);
  } catch (err) {
    console.error("await側のエラー:", err.message);
  }
}
const p = main();
console.log("async関数の戻り値は", p instanceof Promise ? "Promise" : typeof p);

実行結果:

async関数の戻り値は Promise
then: user1
await: user1
then: user2
await側のエラー: 不正なID: 0

最初に「async関数の戻り値は Promise」と表示されている点に注目してください。main()を呼んだ時点では中身はまだ終わっておらず、Promiseだけが先に返っています。then側とawait側の行が交互に出ているのは、2つの処理が並行して進んでいるためです。

Promiseとasync/awaitの違い

観点 Promiseチェーン(then/catch) async/await
書き方 処理を.then()でつないでいく 同期処理と同じように上から順に書ける
エラー処理 .catch()で受ける try/catchで受ける
途中の値の再利用 前の段の値を後の段で使うには変数を外に出すなどの工夫が要る 普通の変数として使える
条件分岐やループ 書けるが読みにくくなりやすい if文やfor文をそのまま使える
並行実行 Promise.all などで表現する 同じくPromise.all などを await する
使える場所 どこでも awaitはasync関数の中か、ES Modulesのトップレベル(ES2022以降)

中身はどちらも同じPromiseなので、混ぜて使えます。たとえばasync関数の結果に.catch()を付けることも、.then()で返るPromiseをawaitすることもできます。

実行される順番を確かめる

awaitの後ろの処理は、すぐには実行されず「マイクロタスク」として後回しにされます。setTimeoutのコールバック(タスク)より先に、同期処理より後に動きます(MDN プロミスの使用)。

console.log("1: 同期処理");
setTimeout(() => console.log("5: setTimeout(タスク)"), 0);
Promise.resolve().then(() => console.log("3: then(マイクロタスク)"));
(async () => {
  console.log("2: async関数の中(awaitの前までは同期)");
  await null;
  console.log("4: awaitの後(マイクロタスク)");
})();

実行結果:

1: 同期処理
2: async関数の中(awaitの前までは同期)
3: then(マイクロタスク)
4: awaitの後(マイクロタスク)
5: setTimeout(タスク)

async関数の中も、最初のawaitまでは同期的に実行されます(2)。awaitの後ろ(4)は、先に予約されていたthen(3)の後に動き、setTimeout(5)は最後です。

よくある落とし穴と直し方

1. 関係のない処理まで順番にawaitしてしまう

互いに依存しない3つの処理を1つずつawaitすると、待ち時間が足し算になります。同時に始めてPromise.allでまとめて待てば、いちばん遅いものの時間で済みます。

const wait = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));

async function sequential() {
  const t = performance.now();
  const a = await wait(300, "A");
  const b = await wait(300, "B");
  const c = await wait(300, "C");
  return [[a, b, c], Math.round((performance.now() - t) / 100) * 100];
}

async function parallel() {
  const t = performance.now();
  const results = await Promise.all([wait(300, "A"), wait(300, "B"), wait(300, "C")]);
  return [results, Math.round((performance.now() - t) / 100) * 100];
}

console.log("順番にawait:", ...(await sequential()), "ms前後");
console.log("Promise.all:", ...(await parallel()), "ms前後");

実行結果(時間は100ミリ秒単位で丸めています):

順番にawait: [ 'A', 'B', 'C' ] 900 ms前後
Promise.all: [ 'A', 'B', 'C' ] 300 ms前後

2. forEachの中でawaitしても待ってくれない

Array.prototype.forEachは、渡した関数が返すPromiseを待ちません。順番に処理したいならfor…of、同時に処理してよいならmapとPromise.allを使います。

const wait = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));
const ids = [1, 2, 3];

const out1 = [];
ids.forEach(async (id) => {
  out1.push(await wait(50, id));
});
console.log("forEach直後:", out1); // 待ってくれない

const out2 = [];
for (const id of ids) {
  out2.push(await wait(50, id));
}
console.log("for...of:", out2);

const out3 = await Promise.all(ids.map((id) => wait(50, id * 10)));
console.log("map + Promise.all:", out3);

実行結果:

forEach直後: []
for...of: [ 1, 2, 3 ]
map + Promise.all: [ 10, 20, 30 ]

3. awaitもcatchもしないPromiseのエラー

async関数を呼ぶだけで、awaitも.catch()もしないと、中で起きたエラーは「処理されなかったrejection」になります。Node.jsはv15.0.0から、この場合の既定の動作を警告からエラーとしてプロセスを終了させる(throwモード)に変えています(Node.js Command-line API –unhandled-rejections)。

async function save() {
  throw new Error("保存に失敗");
}
save(); // awaitも.catchもしない
setTimeout(() => console.log("ここまで来ない"), 100);

実行結果(パスは省略):

Error: 保存に失敗
    at save (file:///.../6_unhandled.mjs:2:9)
    ...
Node.js v24.21.0
終了コード: 1

「ここまで来ない」は表示されず、終了コード1で止まりました。サーバーでこれが起きると、ほかの利用者の処理も巻き込んで止まります。呼び出したPromiseは、必ずawaitするか.catch()を付けてください。

複数のPromiseをまとめるメソッドの使い分け

メソッド 結果 標準になった版 使いどころ
Promise.all 全部成功したら値の配列。1つでも失敗したらその時点で失敗 ES2015 全部そろわないと先に進めない処理
Promise.allSettled 全部終わるまで待ち、成否の一覧を返す ES2020 一部が失敗しても残りの結果を使いたい
Promise.race 最初に決着したもの(成功でも失敗でも) ES2015 タイムアウトとの競争
Promise.any 最初に成功したもの。全部失敗したら失敗 ES2021 複数の取得先のうち、どれか1つ取れればよい

版はTC39の完了提案一覧によるものです(ES2015のものは仕様本体に最初から含まれていたもの)(TC39 Finished Proposals)。4つの違いを実際に動かすと次のようになります。

const ok = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));
const ng = (ms, msg) => new Promise((_, j) => setTimeout(() => j(new Error(msg)), ms));

// all: 1つでも失敗したら、その時点で失敗
try {
  await Promise.all([ok(100, "A"), ng(50, "Bが失敗"), ok(100, "C")]);
} catch (e) {
  console.log("all       →", e.message);
}

// allSettled: 全部終わるまで待ち、成否を一覧で返す
const settled = await Promise.allSettled([ok(100, "A"), ng(50, "Bが失敗")]);
console.log("allSettled→", settled.map((r) => r.status === "fulfilled" ? r.value : r.reason.message));

// race: いちばん早く決着したものを採用(タイムアウトに使える)
const timeout = (ms) => ng(ms, `${ms}msでタイムアウト`);
try {
  await Promise.race([ok(500, "遅い処理"), timeout(200)]);
} catch (e) {
  console.log("race      →", e.message);
}

// any: いちばん早く「成功」したものを採用
console.log("any       →", await Promise.any([ng(50, "ミラー1失敗"), ok(150, "ミラー2"), ok(300, "ミラー3")]));

実行結果:

all       → Bが失敗
allSettled→ [ 'A', 'Bが失敗' ]
race      → 200msでタイムアウト
any       → ミラー2

タイムアウトとキャンセルはAbortSignalで

Promise.raceでタイムアウトを作ると、負けた側の処理は裏で動き続けます。fetchのように中断に対応したAPIなら、AbortSignal.timeout()で指定時間後に本当に処理を止められます(MDN AbortSignal.timeout())。2秒かかるローカルのサーバーに、500ミリ秒のタイムアウトでリクエストした例です。

import http from "node:http";

// 2秒かかる遅いAPIサーバーをローカルに立てる
const server = http.createServer((req, res) => {
  setTimeout(() => res.end("ok"), 2000);
}).listen(0);
const url = `http://127.0.0.1:${server.address().port}/`;

try {
  const res = await fetch(url, { signal: AbortSignal.timeout(500) });
  console.log(await res.text());
} catch (e) {
  console.log(e.name, "-", e.message);
} finally {
  server.closeAllConnections();
  server.close();
}

実行結果:

TimeoutError - The operation was aborted due to timeout

タイムアウトの場合、エラーの名前はTimeoutErrorになります。利用者が「キャンセル」を押したときなど自分で止めたい場合は、AbortControllerを作ってabort()を呼びます。

最近追加されたPromise関連の機能

ここ数年で、Promiseを扱う道具が少しずつ増えています(TC39 Finished Proposals)。

  • Promise.withResolvers(ES2024):Promiseと、それを外から解決・拒否する関数を一度に作る
  • Promise.try(ES2025):関数を呼び、同期的に投げられた例外もrejectedのPromiseとして扱う。MDNではBaseline 2025(2025年1月以降、主要ブラウザの最新版で利用可能)とされています(MDN Promise.try())
  • Array.fromAsync(ES2026):非同期イテラブルから配列を作る
// ES2024: Promise.withResolvers
const { promise, resolve } = Promise.withResolvers();
setTimeout(() => resolve("外から解決"), 50);
console.log(await promise);

// ES2025: Promise.try(同期の例外もPromiseの失敗として扱える)
const r = await Promise.try(() => JSON.parse("{壊れたJSON")).catch((e) => e.name);
console.log("Promise.try:", r);

// ES2026: Array.fromAsync
async function* gen() { yield 1; yield 2; yield 3; }
console.log("Array.fromAsync:", await Array.fromAsync(gen()));

実行結果:

外から解決
Promise.try: SyntaxError
Array.fromAsync: [ 1, 2, 3 ]

Node.js v24ではどれも使えましたが、古いブラウザや古いNode.jsを対象にする場合は、対応状況を確認してから使ってください。

結局どちらを使えばよいか

  • 新しく書くコードは、async/awaitを基本にする。エラー処理はtry/catchでまとめる
  • 互いに依存しない処理は、先に全部始めてからPromise.all(一部の失敗を許すならallSettled)でawaitする
  • イベントのコールバックなど、async関数で包めない場所や、短い変換を1行で書きたい場所では.then()/.catch()も使う
  • どちらの書き方でも、作ったPromiseは必ずawaitするか.catch()を付ける

非同期処理の考え方そのものは非同期処理とは?、JavaScript全体の学び方はJavaScript入門ガイド、実行環境についてはNode.jsとは?、ほかの言語での非同期処理の例はKotlinのコルーチンとは?で解説しています。

よくある質問

Q. Promiseとasync/awaitの違いは何ですか?

async/awaitはPromiseを読みやすく書くための構文で、中身は同じPromiseです。Promiseチェーンは.then()と.catch()で処理をつなぎ、async/awaitは同期処理のように上から順に書いてtry/catchでエラーを受けます。

Q. async関数は何を返しますか?

常にPromiseを返します。関数の中でreturnした値はPromiseの成功時の値になり、throwしたエラーは失敗時の理由になります。呼び出し側では、awaitするか.then()で結果を受け取ります。

Q. 複数の処理を同時に実行するにはどうすればよいですか?

処理を先に全部始め、Promise.allでまとめてawaitします。一部が失敗しても残りの結果を使いたいならPromise.allSettledを使います。1つずつawaitすると、待ち時間が足し算になります。

Q. forEachの中でawaitが効かないのはなぜですか?

forEachは渡した関数が返すPromiseを待たないためです。順番に処理するならfor…of、同時に処理してよいならmapで作ったPromiseの配列をPromise.allでawaitします。

Q. awaitしなかったPromiseのエラーはどうなりますか?

処理されなかったrejectionになります。Node.jsはv15.0.0から既定でこれをエラーとして扱い、プロセスを終了させます。作ったPromiseは必ずawaitするか.catch()を付けてください。

非同期処理のように、本を読むだけではつかみにくい部分を手を動かして覚えたい人は、通学型プログラミングスクールのテックジムで、自分のペースで課題を進め、つまずいたらトレーナーに質問しながら学ぶ方法もあります。無料講座から試せます。

プログラミング学習に興味がある方は、まずは無料体験会へ

無料体験会・お申し込みはこちら