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






