非同期処理の基本 (Promise と async/await) ~ JavaScript の非同期を理解する 第 弾
JavaScript の非同期を理解する記事の第 7 弾です。
前回は jQuery の Deferred オブジェクトについて学びました。Callback 地獄への対策として、非常に利用が多かった Deferred オブジェクトですが、今ではあまり利用されなくなってきました。
なぜなら、ES2015 で JavaScript 自体に Promise
が導入されたことで、jQuery を利用せずとも同じような構文で非同期処理を扱うことができるようになったからです。
Promise
自体の概念は、前章で説明した jQuery Deferred の説明と被る部分も多く割愛させていただき、基本的な使い方を紹介します。
Promise の使い方
前章と被る部分もあるかもしれませんが、Promise
の基本的な使い方を紹介します。
function promiseFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("success");
}, 1000);
});
}
promiseFunction()
.then((result) => {
console.log(result);
})
.catch((err) => {
console.error(err);
});
ここで押さえておきたいポイントは以下のとおりです。
-
Promise
コンストラクタは内部的に状態(Pending → Fulfilled/Rejected)を実装し、executor 関数は同期的に実行されます (executor 関数は(resolve, reject) => {...}
を指します) -
resolve
/reject
は状態遷移をトリガーし、状態に応じて後続の.then()
に登録された関数を マイクロタスクキュー に 追加します - executor 関数内での例外は自動的に
reject
扱いとなるため、後続のcatch
でエラーを処理することができます -
.then()
は常に新しい Promise を返すため、逐次処理をチェーンとして記述可能です
前章は jQuery Deferred の Promise についての説明でしたが、ベースとなる概念は同じで、挙動としても 3.0 以降は同じようになっています。
then
や catch
以外にもさまざまなメソッドが用意されていますが、基本となる考え方は変わらず、状態による処理条件などが変わるなどの違いとなっています。
async / await の登場
さて、Promise
の登場によって Callback 地獄を抜けたかに思えた非同期処理ですが、まだ課題は完全に解決されたわけではありませんでした。
例えば、非同期処理の結果によって条件分岐をする場合のコードを見てみます。
// 一つ目の非同期処理結果に合わせて、条件分岐
getUserAsync(userId)
.then(function (user) {
const logAccessAsyncResult = logAccessAsync(user);
if (userAsyncResult) {
return sendEmailAsync(user.email);
} else {
return Promise.reject(new Error("ログアクセス失敗"));
}
})
.then(function (sendEmailAsyncResult) {
console.log("メール送信完了");
})
.catch(function (err) {
console.error("処理失敗:", err);
});
Callback 関数をネストする時ほどではありませんが、ネスト構造が深くなっています。処理を追うにしても、Promise
のチェーン処理を理解した上で読む必要があり、読みにくさは完全に解消されたわけではありません。
そこで、async / await
が登場しました。
// async / await による非同期処理のチェーン
async function handleUser(userId) {
try {
const user = await getUserAsync(userId);
const logAccessAsyncResult = await logAccessAsync(user);
if (!logAccessAsyncResult) {
throw new Error("ログアクセス失敗");
}
await sendEmailAsync(user.email);
console.log("メール送信完了");
} catch (err) {
console.error("処理失敗:", err);
}
}
handleUser(userId);
比較してみると、かなり処理を追いやすく読みやすくなっています。これはネストになっていないこともそうですが、コードの流れが、逐次的処理を書くコードと同じ構造となっていることが大きな要因です。またエラー処理も try-catch
でシンプルに書くことができており、これも非同期処理ならではの処理をあまり理解せずとも、全体の挙動を直感的に理解することが可能となっています。
async / await を使わない方がいいことはある?
このようにいい事づくめに見える、async / await
ですが、その正体は、Promise
処理をより直感的に書くための構文です。
そのため、場合によっては async/await
を使わずに Promise
を利用した方が良いケースもあります。いかに一つの例を挙げてみます。
Promise を利用した並行処理:
-
await
は各呼び出しでマイクロタスクを作成(オーバーヘッド) - 並行実行には
Promise.all()
/Promise.allSettled()
を使用
// 逐次実行(遅い)
const a = await fetchA();
const b = await fetchB();
// 並行実行(速い)
const [a, b] = await Promise.all([fetchA(), fetchB()]);
上記はあくまでサンプルです。実際の実行時には、JS エンジンで構文解析と最適化が行われるため、async/await
を利用して問題ないことも多いと思います。結論としては、ケースバイケース、となってしまいますが、とりあえず async / await
を使う、のではなく、どちらも選択肢として持つことが大事だと思います。
コードを実行してみよう
async/await でのエラーハンドリングパターン
async function fetchUser(userId) {
try {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = await res.json();
console.log("ユーザー取得成功:", data);
return data;
} catch (err) {
console.error("エラー:", err.message);
throw err;
}
}
(async () => {
try {
await fetchUser("123");
} catch {
console.log("最終的に失敗");
}
})();
並行処理の最適化パターン
// Promiseを返すモック関数(実行時間をシミュレート)
function fetchUser(id) {
return new Promise((resolve) =>
setTimeout(() => {
console.log(`ユーザー${id}を取得完了`);
resolve({ id, name: `ユーザー${id}` });
}, 1000)
);
}
function fetchPosts(userId) {
return new Promise((resolve) =>
setTimeout(() => {
console.log(`ユーザー${userId}の投稿を取得完了`);
resolve([`投稿1`, `投稿2`]);
}, 1000)
);
}
function fetchFriends(userId) {
return new Promise((resolve) =>
setTimeout(() => {
console.log(`ユーザー${userId}の友達を取得完了`);
resolve([`友達A`, `友達B`]);
}, 1000)
);
}
// 逐次実行: 合計3秒かかる
async function sequential() {
console.time("逐次実行の時間");
const user = await fetchUser(1);
const posts = await fetchPosts(1);
const friends = await fetchFriends(1);
console.timeEnd("逐次実行の時間");
return { user, posts, friends };
}
// 並列実行: 1秒で完了
async function parallel() {
console.time("並列実行の時間");
const [user, posts, friends] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchFriends(1),
]);
console.timeEnd("並列実行の時間");
return { user, posts, friends };
}
// 実行して比較
(async () => {
console.log("=== 逐次実行 ===");
const seqResult = await sequential();
console.log("結果:", seqResult);
console.log("\n=== 並列実行 ===");
const parallelResult = await parallel();
console.log("結果:", parallelResult);
})();
まとめ
この記事では、Promise と async/await について学びました。Promise
は、非同期処理の結果を管理するオブジェクトであり、async/await
は、Promise
をより直感的に扱うための構文です。
async/await
は、今では当たり前のように利用されていますが、実際には Promise
処理が内部的に行われているということを意識して利用することが重要だと思いました。
さて、今回は Promise と async/await について以下を学びました。
- async / await の登場背景
- Promise と async/await の基本的な使い方
次回
さて第 8 章 最後の記事は、 「まとめ」 になります。
この記事は、JavaScript について勉強した内容をまとめたものであり、内容が不正確な可能性があります。もし指摘などあれば、コメントいただけるととても嬉しいです。
参考資料
Futures and Promises
JavaScript Promise の本
JavaScript の非同期処理をじっくり理解する