JavaScriptのasync/await入門|非同期処理の基本
JavaScriptの非同期処理は、APIからデータを取得するときやファイルを読み込むときなど、あらゆる場面で使われます。async/awaitを使うと、複雑になりがちな非同期処理を同期処理のように直感的に書けます。この記事では、非同期処理の基礎からPromise、async/await、fetch APIの活用までを順を追って解説します。
非同期処理とは何か
JavaScriptはシングルスレッドの言語です。つまり、一度に1つの処理しか実行できません。しかし、サーバーへのリクエストやタイマー処理のように時間がかかる処理を待っている間、他の処理を止めてしまうとユーザー体験が大きく損なわれます。
同期処理と非同期処理の違い
同期処理は、上から順に1行ずつ実行され、前の処理が終わるまで次の処理に進みません。
// 同期処理の例
console.log("1番目");
console.log("2番目");
console.log("3番目");
// 出力: 1番目 → 2番目 → 3番目(この順番で必ず表示される)
非同期処理では、時間のかかる処理を「後で完了を受け取る」形にして、その間に他の処理を進めます。
// 非同期処理の例
console.log("1番目");
setTimeout(() => {
console.log("2番目(1秒後)");
}, 1000);
console.log("3番目");
// 出力: 1番目 → 3番目 → 2番目(1秒後)
なぜ非同期処理が必要なのか
Webアプリケーションでは、次のような場面で非同期処理が不可欠です。
- サーバーからデータを取得する(API通信)
- ユーザーの入力を待つ(イベント処理)
- 一定時間後に処理を実行する(タイマー)
- ファイルの読み書き(Node.js環境)
これらの処理を同期的に行うと、処理が完了するまでページ全体が固まってしまいます。
コールバック関数の問題点
非同期処理の最も古いパターンが「コールバック関数」です。処理が完了したときに呼ばれる関数を引数として渡します。
コールバックの基本
function fetchData(callback) {
setTimeout(() => {
callback("データを取得しました");
}, 1000);
}
fetchData((result) => {
console.log(result); // "データを取得しました"
});
コールバック地獄
コールバックが入れ子になると、コードの可読性が著しく低下します。この状態を「コールバック地獄」と呼びます。
// コールバック地獄の例
getUser(userId, (user) => {
getOrders(user.id, (orders) => {
getOrderDetails(orders[0].id, (details) => {
getProductInfo(details.productId, (product) => {
console.log(product.name);
// インデントがどんどん深くなる...
});
});
});
});
この問題を解決するために、Promiseが導入されました。
Promiseの基本
Promiseは、非同期処理の「結果をいずれ返す」という約束を表すオブジェクトです。ES2015(ES6)で正式に導入されました。
Promiseの3つの状態
Promiseには次の3つの状態があります。
- pending(待機中): まだ結果が確定していない
- fulfilled(成功): 処理が成功し、結果の値がある
- rejected(失敗): 処理が失敗し、エラーの理由がある
Promiseの作成と使い方
// Promiseを作成する
const promise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve("成功しました"); // fulfilled状態にする
} else {
reject("失敗しました"); // rejected状態にする
}
});
// Promiseの結果を受け取る
promise
.then((result) => {
console.log(result); // "成功しました"
})
.catch((error) => {
console.log(error);
});
thenチェーンで直列処理
.then()を繋げることで、非同期処理を順番に実行できます。コールバック地獄と比べて読みやすくなります。
getUser(userId)
.then((user) => getOrders(user.id))
.then((orders) => getOrderDetails(orders[0].id))
.then((details) => getProductInfo(details.productId))
.then((product) => {
console.log(product.name);
})
.catch((error) => {
console.error("エラーが発生:", error);
});
async/awaitの基本
async/awaitはES2017で導入された構文で、Promiseをさらに簡潔に書けるようにしたものです。Promiseの上に構築された「糖衣構文(シンタックスシュガー)」です。
async関数の宣言
asyncキーワードを関数の前につけると、その関数は必ずPromiseを返すようになります。
// async関数は常にPromiseを返す
async function greet() {
return "こんにちは";
}
// 上のコードは以下と同じ意味
function greet() {
return Promise.resolve("こんにちは");
}
greet().then((message) => {
console.log(message); // "こんにちは"
});
awaitで結果を待つ
awaitキーワードを使うと、Promiseの結果が返されるまで処理を一時停止できます。awaitはasync関数の中でのみ使えます。
async function fetchUserData() {
// awaitでPromiseの結果を待つ
const user = await getUser(userId);
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
const product = await getProductInfo(details.productId);
console.log(product.name);
}
コールバック地獄やthenチェーンと比べて、同期処理のように上から下へ読めるようになりました。
async/awaitのエラーハンドリング
async/awaitでは、try/catch文を使ってエラーを処理します。
async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);
} catch (error) {
console.error("データの取得に失敗しました:", error.message);
}
}
fetch APIとの組み合わせ
fetch APIは、HTTPリクエストを送信するためのブラウザ標準のAPIです。async/awaitと組み合わせることで、API通信のコードが非常に読みやすくなります。
GETリクエストの基本
async function getUsers() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
// レスポンスのステータスを確認
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
const users = await response.json();
console.log(users);
} catch (error) {
console.error("取得エラー:", error.message);
}
}
POSTリクエストとresponse.okの確認
データを送信する場合は、fetchの第2引数にオプションを指定します。また、fetchはHTTPステータスが404や500でもPromiseをrejectしないため、response.okで手動チェックが必要です。
async function createUser(name, email) {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name, email }),
});
// response.okがfalseなら404や500などのエラー
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
const newUser = await response.json();
console.log("作成されたユーザー:", newUser);
} catch (error) {
console.error("送信エラー:", error.message);
}
}
複数の非同期処理を並列実行する
複数のAPIリクエストを同時に送りたい場合、Promise.allを使うと効率的です。
Promise.allの使い方
async function fetchMultipleData() {
try {
// 3つのリクエストを同時に実行
const [users, posts, comments] = await Promise.all([
fetch("https://jsonplaceholder.typicode.com/users").then(r => r.json()),
fetch("https://jsonplaceholder.typicode.com/posts").then(r => r.json()),
fetch("https://jsonplaceholder.typicode.com/comments").then(r => r.json()),
]);
console.log("ユーザー数:", users.length);
console.log("投稿数:", posts.length);
console.log("コメント数:", comments.length);
} catch (error) {
console.error("いずれかのリクエストが失敗:", error.message);
}
}
Promise.allは、すべてのPromiseが成功すると結果の配列を返します。1つでも失敗すると、即座にcatchに入ります。
よくある間違いと注意点
async/awaitを使い始めたときに陥りやすいポイントをまとめます。
awaitの付け忘れ
awaitを書き忘れると、Promiseオブジェクトそのものが変数に入ります。
// NG: awaitがないのでPromiseオブジェクトが入る
async function getData() {
const data = fetch("https://api.example.com/data");
console.log(data); // Promise { <pending> }
}
// OK: awaitを付ける
async function getData() {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data); // 実際のデータ
}
async関数の外でawaitを使う
awaitはasync関数の中でのみ使用できます。トップレベルで使いたい場合は、即時実行関数を使うか、モジュールのトップレベルawait(ES2022)を使います。
// NG: 通常の関数内ではawaitは使えない
function getData() {
const data = await fetch("/api/data"); // SyntaxError
}
// OK: async関数の中で使う
async function getData() {
const data = await fetch("/api/data");
}
// OK: トップレベルawait(ESモジュール内)
const response = await fetch("/api/data");
const data = await response.json();
ループ内でのawaitに注意
forループ内でawaitを使うと、1つずつ順番に実行されるため遅くなることがあります。
// 遅い: 1つずつ順番にリクエスト
async function fetchAllSlow(urls) {
const results = [];
for (const url of urls) {
const response = await fetch(url);
results.push(await response.json());
}
return results;
}
// 速い: 同時にリクエストして全部待つ
async function fetchAllFast(urls) {
const promises = urls.map(url =>
fetch(url).then(r => r.json())
);
return Promise.all(promises);
}
まとめ
JavaScriptの非同期処理について、コールバックからPromise、async/awaitまでの流れを解説しました。ポイントを振り返ります。
- 非同期処理は、時間のかかる処理を待つ間に他の処理を進める仕組み
- Promiseは非同期処理の結果を表すオブジェクトで、then/catchで結果を受け取る
- async/awaitはPromiseを同期処理のように書ける構文
- fetch APIとasync/awaitを組み合わせると、API通信のコードが読みやすくなる
- 複数の非同期処理を並列実行するにはPromise.allを使う
- awaitの付け忘れやループ内での使用に注意する
まずはfetchとasync/awaitを組み合わせた簡単なAPIリクエストから試してみてください。実際にコードを書いて動かすことで、非同期処理の流れが体感的にわかるようになります。