はじめに
現代のJavaScriptには、 async / await という、非同期処理をまるで同期処理(上から順番に実行される普通の処理)のように書ける、魔法のような機能があります。
この記事では、async/await が「なんやねん」状態の人が、「なるほど、こういうことか!」と完全に理解した気になれるよう、わかりやすく解説していきます。
そもそも「非同期処理」って?
async/await を知る前に、なぜそれが必要なのか、つまり「非同期処理」が何かを知る必要があります。
-
同期処理
- 一つのタスクが終わるまで、次のタスクは待機します。
- 例:レジに並ぶお客さん。前の人が終わらないと自分の番は来ません。
-
非同期処理
- 時間のかかるタスク(例:サーバーからデータを取ってくる、ファイルを読み込む)を待たずに、次のタスクに進みます。タスクが終わったら、後で結果を受け取ります。
- 例:レストランの注文。注文(タスク)を伝えたら、あなたは席で待ち(他のことができる)、料理ができたら(タスク完了)店員さんが持ってきてくれます。
JavaScriptは基本的にシングルスレッド(一度に一つのことしかできない)ですが、この非同期処理のおかげで、重い処理を待っている間もブラウザが固まったりせず、スムーズに動作できるのです!
Promise(約束)という前提
async/await は、実は Promise(プロミス) という仕組みを、もっと読みやすく、書きやすくするための構文です。
Promiseは「非同期処理の最終的な結果を表すオブジェクト」です。
平たく言えば、「今はまだ結果が出てないけど、いつか必ず結果(成功か失敗か)を返すという『約束』」です。
従来のPromiseの書き方を見てみましょう。
// データを取ってくる非同期関数(Promiseを返す)
function fetchData() {
return new Promise((resolve, reject) => {
// 2秒後にデータを返すシミュレーション
setTimeout(() => {
resolve("データ取得成功!");
// もし失敗したら reject("エラー発生!");
}, 2000);
});
}
// Promiseを使った処理
fetchData()
.then(data => {
console.log(data); // 2秒後に "データ取得成功!"
// さらに別の非同期処理...
return anotherFetch(data);
})
.then(nextData => {
console.log(nextData);
})
.catch(error => {
console.error("エラー", error);
});
このように .then() を使って、処理が成功した「後」の動作をチェーン(鎖)のようにつなげていきます。
便利ですが、処理が複雑になると、このチェーンが長くなり、読みにくくなることがありました。
「async/await」の使い方
お待たせしました。本題の async/await です。
これらはセットで使います。
1. async (非同期関数ですよ宣言)
関数を定義するとき、function の前に async を付けます。
async function myAsyncFunction() {
// ...
}
// アロー関数なら
const myAsyncFunction = async () => {
// ...
};
async を付けた関数は、必ず Promiseを返す 関数になります。
(もし関数内で何らかのreturn (例:return 123)がされても、自動的に Promise.resolve(123) のようにPromiseで包まれて返されます)
2. await (待っててね命令)
async を付けた関数の中でだけ使える特別なキーワードです。
await は、Promiseの結果が出るまで、その場(関数内)の処理を一時停止させます。
さっきのPromiseの例を async/await で書き換えてみましょう。
// データを取ってくる非同期関数(中身は同じ)
function fetchData() {
return new Promise(resolve => {
setTimeout(() => {
resolve("データ取得成功!");
}, 2000);
});
}
// async/awaitを使った処理
async function processData() {
console.log("処理開始");
// fetchData() が終わる(Promiseが解決される)までここで待つ
// 結果が data に格納される
const data = await fetchData();
console.log(data); // 2秒後に "データ取得成功!"
console.log("処理終了");
}
processData();
/*
実行結果↓
処理開始
↓
(2秒待機)
↓
データ取得成功!
↓
処理終了
*/
どうでしょうか?
.then() のチェーンがなくなり、まるで上から順番に実行される同期処理のように読めませんか?
const data = await fetchData(); の部分がキモです。
Promiseの結果(resolveされた値)を、await が「待って」くれて、変数 data に直接代入してくれているのです。
エラーハンドリング (try...catch)
「.then() がないなら、エラーを捕まえる .catch() はどうなるの?」
良い質問です。async/await では、同期処理で使うおなじみの try...catch 構文を使います。
async function processDataWithError() {
try {
console.log("処理開始");
// わざと失敗するPromise
const data = await new Promise((resolve, reject) => {
setTimeout(() => reject("ネットワークエラー!"), 2000);
});
console.log(data); // ここは実行されない
} catch (error) {
// await したPromiseが reject されると、ここでキャッチできる
console.error("エラーが発生しました", error); // 2秒後に "エラーが発生しました ネットワークエラー!"
} finally {
console.log("処理完了(成功しても失敗しても実行)");
}
}
processDataWithError();
これも、普段のJavaScriptのエラー処理と同じ書き方なので、非常に直感的ですね。
押さえておくべき3つの注意点
async/await は強力ですが、いくつか知っておくべきルールがあります。
1. await は async 関数の中でしか使えない
async が付いていない普通の関数や、グローバルスコープ(関数の外)で await を使おうとすると、エラーになります。
// ダメな例
function normalFunction() {
const data = await fetchData(); // SyntaxErrorになる
}
補足:トップレベル await
2025年現在、ESM(ECMAScript Modules)と呼ばれるモジュールシステムの中では、async で囲まなくても await を使える「トップレベルawait」がサポートされています。
しかし、基本的なルールとして「await は async の中」と覚えておけば間違いありません。
2. await の使いすぎは「遅く」なる
async/await は、処理を「待たせる」機能です。
もし、互いに関係のない複数の非同期処理を、すべて await で待ってしまうと、無駄な待ち時間が発生します。
// 遅い例(逐次処理)
async function fetchAllSequentially() {
console.time("sequential");
const data1 = await fetchData(1); // 2秒待つ
const data2 = await fetchData(2); // さらに2秒待つ
const data3 = await fetchData(3); // さらに2秒待つ
console.timeEnd("sequential"); // 約6秒かかる
}
data1 と data2 と data3 に関連がないなら、同時にリクエストを開始したいですよね。
そういう時は、Promise.all を使います。
// 速い例(並列処理)
async function fetchAllParallel() {
console.time("parallel");
// 3つの非同期処理を「同時に」開始する
const promise1 = fetchData(1);
const promise2 = fetchData(2);
const promise3 = fetchData(3);
// 3つすべてが終わるのを「待つ」
const [data1, data2, data3] = await Promise.all([promise1, promise2, promise3]);
console.timeEnd("parallel"); // 約2秒で終わる
}
async/await を使いつつも、処理の依存関係を意識し、並列化できるところは Promise.all を使うのがパフォーマンス向上のコツです。
3. async 関数は「常に」Promiseを返す
async 関数を呼び出したときの戻り値は、常にPromiseです。
async function getNumber() {
return 10;
}
const result = getNumber();
console.log(result); // 10 ではなく、 Promise { <fulfilled>: 10 } と出力される
もし async 関数の結果を別の場所で使いたい場合は、呼び出し側も await するか、.then() を使う必要があります。
// 呼び出し側も async/await
async function useNumber() {
const num = await getNumber();
console.log(num); // 10
}
// .then を使う
getNumber().then(num => {
console.log(num); // 10
});
まとめ
async/await を使えば、JavaScriptの非同期処理が、まるで上から下に流れる同期処理のように、直感的に読み書きできます。
- 関数に
asyncを付けて「非同期関数」を宣言する。 -
async関数の中で、Promiseの結果をawaitで「待つ」。 - エラー処理は
try...catchで囲む。
もう .then() の連鎖やコールバック地獄に悩まされる必要はありません。
まずは簡単な処理から、async/awaitの使い方について徐々に慣れていきましょう!