はじめに#
JavaScriptで非同期処理を書くとき、「Promise」と「async/await」という2つの書き方を目にすることがあります。どちらも非同期処理を扱う仕組みですが、async/awaitを使うと、まるで同期処理のように直感的なコードを書けるようになります。
ES2017(ES8)で導入されたasync/awaitは、Promiseをベースにした構文糖衣(シンタックスシュガー)です。Promiseチェーンの複雑さを解消し、可読性の高いコードを実現します。
本記事では、以下の内容を初心者向けにわかりやすく解説します。
- async/awaitの基本構文と動作原理
- Promiseとの違いと使い分け
- try-catchによるエラーハンドリング
- 複数の非同期処理を並列実行する方法
- 実践的なAPI通信の例
async/awaitとは#
基本概念#
async/awaitは、非同期処理を同期処理のように書けるJavaScript の構文です。asyncキーワードを関数に付けると「非同期関数」になり、その中でawaitキーワードを使ってPromiseの完了を待つことができます。
1
2
3
4
5
6
|
// async関数の基本形
async function fetchData() {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
return data;
}
|
上記のコードでは、fetchとresponse.json()という2つの非同期処理を順番に実行しています。awaitにより、各処理が完了するまで次の行に進まないため、同期処理のように上から下へ読めます。
asyncキーワードの役割#
asyncキーワードを関数の前に付けると、その関数は必ずPromiseを返すようになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// async関数は必ずPromiseを返す
async function greet() {
return "Hello!";
}
// 上記は以下と等価
function greet() {
return Promise.resolve("Hello!");
}
// 戻り値の確認
greet().then((message) => {
console.log(message); // "Hello!"
});
|
async関数内でreturnした値は、自動的にPromise.resolve()でラップされます。
awaitキーワードの役割#
awaitキーワードは、Promiseが解決(fulfilled)または拒否(rejected)されるまで処理を一時停止します。Promiseが解決されると、その結果値が返されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
async function example() {
console.log("1. 開始");
// 1秒待機するPromiseをawait
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log("2. 1秒後に実行");
// さらに1秒待機
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log("3. 2秒後に実行");
}
example();
console.log("4. async関数を呼び出した直後");
// 出力順序:
// 1. 開始
// 4. async関数を呼び出した直後
// 2. 1秒後に実行(1秒後)
// 3. 2秒後に実行(2秒後)
|
awaitはasync関数の中でのみ使用可能です。通常の関数内で使うとSyntaxErrorが発生します。
1
2
3
4
|
// エラー: awaitはasync関数内でのみ使用可能
function normalFunction() {
await fetch("https://api.example.com/data"); // SyntaxError
}
|
ただし、ES2022以降では、モジュールのトップレベルでawaitを使用できる「Top-level await」がサポートされています。
1
2
3
4
|
// ESモジュールのトップレベルでのawait(ES2022以降)
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);
|
Promiseとasync/awaitの比較#
同じ処理の書き方の違い#
Promiseチェーンとasync/awaitで同じ処理を書いた場合の違いを見てみましょう。
Promiseチェーンを使った書き方
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
function fetchUserData(userId) {
return fetch(`https://api.example.com/users/${userId}`)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then((user) => {
return fetch(`https://api.example.com/posts?userId=${user.id}`);
})
.then((response) => response.json())
.then((posts) => {
console.log("ユーザーの投稿:", posts);
return posts;
})
.catch((error) => {
console.error("エラー:", error);
});
}
|
async/awaitを使った書き方
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
async function fetchUserData(userId) {
try {
const userResponse = await fetch(`https://api.example.com/users/${userId}`);
if (!userResponse.ok) {
throw new Error(`HTTP error: ${userResponse.status}`);
}
const user = await userResponse.json();
const postsResponse = await fetch(`https://api.example.com/posts?userId=${user.id}`);
const posts = await postsResponse.json();
console.log("ユーザーの投稿:", posts);
return posts;
} catch (error) {
console.error("エラー:", error);
}
}
|
async/await版は、処理の流れが上から下へ一直線に読めるため、可読性が大幅に向上しています。
比較表#
| 観点 |
Promise(.then/.catch) |
async/await |
| 可読性 |
チェーンが長いと複雑になりやすい |
同期的なコードのように読みやすい |
| エラー処理 |
.catch()で処理 |
try-catchで処理 |
| デバッグ |
スタックトレースが追いにくい |
通常の関数と同様に追いやすい |
| 条件分岐 |
ネストが深くなりやすい |
通常のif文で記述可能 |
| 互換性 |
ES2015(ES6)以降 |
ES2017(ES8)以降 |
処理フローの図解#
async/await使用時の処理フローを図で確認してみましょう。
sequenceDiagram
participant Main as メインスレッド
participant API as API Server
Main->>Main: async関数を呼び出し
Main->>API: await fetch() - リクエスト送信
Note over Main: 処理を一時停止<br/>(他のコードは実行可能)
API-->>Main: レスポンス返却
Main->>Main: await response.json()
Note over Main: 処理を一時停止
Main->>Main: JSON解析完了
Main->>Main: 次の処理を実行エラーハンドリング#
try-catchによる基本的なエラー処理#
async/awaitでは、同期処理と同様にtry-catch文でエラーを捕捉できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
if (!response.ok) {
throw new Error(`HTTPエラー: ${response.status}`);
}
const data = await response.json();
console.log("取得成功:", data);
return data;
} catch (error) {
console.error("エラーが発生しました:", error.message);
return null;
}
}
|
finallyによるクリーンアップ処理#
finallyブロックを使うと、成功・失敗に関わらず必ず実行したい処理を記述できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
async function loadData() {
const loadingIndicator = document.getElementById("loading");
try {
loadingIndicator.style.display = "block"; // ローディング表示
const response = await fetch("https://api.example.com/data");
const data = await response.json();
return data;
} catch (error) {
console.error("データの取得に失敗しました:", error);
throw error;
} finally {
loadingIndicator.style.display = "none"; // ローディング非表示(必ず実行)
}
}
|
エラーの種類に応じた処理#
エラーの種類を判別して、適切な対応を行うことができます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
async function fetchWithRetry(url, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) {
// HTTPステータスに応じたエラー処理
if (response.status === 404) {
throw new Error("リソースが見つかりません");
} else if (response.status === 401) {
throw new Error("認証が必要です");
} else if (response.status >= 500) {
throw new Error("サーバーエラー");
}
}
return await response.json();
} catch (error) {
console.log(`試行 ${attempt}/${maxRetries} 失敗:`, error.message);
if (error.name === "TypeError") {
// ネットワークエラー(fetch自体の失敗)
console.log("ネットワーク接続を確認してください");
}
if (attempt === maxRetries) {
throw new Error(`${maxRetries}回の試行後も失敗: ${error.message}`);
}
// リトライ前に待機
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
}
|
エラー処理のベストプラクティス#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
// カスタムエラークラスの定義
class ApiError extends Error {
constructor(message, statusCode) {
super(message);
this.name = "ApiError";
this.statusCode = statusCode;
}
}
// エラーを適切に処理する関数
async function fetchApi(endpoint) {
try {
const response = await fetch(`https://api.example.com${endpoint}`);
if (!response.ok) {
throw new ApiError(
`API request failed: ${response.statusText}`,
response.status
);
}
return await response.json();
} catch (error) {
if (error instanceof ApiError) {
// APIエラーの場合
console.error(`API Error (${error.statusCode}):`, error.message);
} else if (error instanceof TypeError) {
// ネットワークエラーの場合
console.error("Network Error:", error.message);
} else {
// その他のエラー
console.error("Unexpected Error:", error);
}
throw error; // エラーを再スロー
}
}
|
複数の非同期処理の実行パターン#
逐次実行(順番に実行)#
前の処理の結果を次の処理で使う場合は、順番にawaitします。
1
2
3
4
5
6
7
8
9
10
|
async function sequentialExecution() {
console.log("開始");
const user = await fetchUser(1); // 1秒かかると仮定
const posts = await fetchPosts(user.id); // 1秒かかると仮定
const comments = await fetchComments(posts[0].id); // 1秒かかると仮定
console.log("完了"); // 合計3秒後
return { user, posts, comments };
}
|
並列実行(Promise.all)#
互いに依存しない処理は、Promise.allで並列に実行すると効率的です。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
async function parallelExecution() {
console.log("開始");
// 3つのリクエストを同時に開始
const [users, products, orders] = await Promise.all([
fetch("https://api.example.com/users").then((r) => r.json()),
fetch("https://api.example.com/products").then((r) => r.json()),
fetch("https://api.example.com/orders").then((r) => r.json()),
]);
console.log("完了"); // 最も遅いリクエストが完了した時点
return { users, products, orders };
}
|
gantt
title 並列実行 vs 逐次実行
dateFormat X
axisFormat %s秒
section 逐次実行
ユーザー取得 :0, 1
商品取得 :1, 2
注文取得 :2, 3
section 並列実行
ユーザー取得 :0, 1
商品取得 :0, 1
注文取得 :0, 1Promise.allSettled(すべての結果を取得)#
Promise.allは1つでも失敗すると全体が失敗扱いになりますが、Promise.allSettledはすべての結果(成功・失敗両方)を取得できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
async function fetchAllData() {
const results = await Promise.allSettled([
fetch("https://api.example.com/users").then((r) => r.json()),
fetch("https://invalid-url.com/data").then((r) => r.json()), // 失敗する
fetch("https://api.example.com/products").then((r) => r.json()),
]);
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`リクエスト${index + 1} 成功:`, result.value);
} else {
console.log(`リクエスト${index + 1} 失敗:`, result.reason);
}
});
// 成功した結果のみを抽出
const successfulResults = results
.filter((r) => r.status === "fulfilled")
.map((r) => r.value);
return successfulResults;
}
|
Promise.race(最初に完了した結果を取得)#
Promise.raceは、最初に完了(成功または失敗)したPromiseの結果を返します。タイムアウト処理の実装などに使えます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// タイムアウト付きのfetch
async function fetchWithTimeout(url, timeoutMs = 5000) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("タイムアウト")), timeoutMs);
});
const fetchPromise = fetch(url).then((r) => r.json());
return Promise.race([fetchPromise, timeoutPromise]);
}
// 使用例
async function getData() {
try {
const data = await fetchWithTimeout("https://api.example.com/data", 3000);
console.log("データ取得成功:", data);
} catch (error) {
if (error.message === "タイムアウト") {
console.log("リクエストがタイムアウトしました");
} else {
console.log("エラー:", error.message);
}
}
}
|
Promise.any(最初に成功した結果を取得)#
Promise.anyは、最初に成功したPromiseの結果を返します。すべて失敗した場合のみエラーになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
async function fetchFromMultipleSources() {
try {
// 複数のミラーサーバーから最初に成功したものを使用
const data = await Promise.any([
fetch("https://server1.example.com/data").then((r) => r.json()),
fetch("https://server2.example.com/data").then((r) => r.json()),
fetch("https://server3.example.com/data").then((r) => r.json()),
]);
console.log("データ取得成功:", data);
return data;
} catch (error) {
console.error("すべてのサーバーから取得に失敗しました");
throw error;
}
}
|
実践的なコード例#
APIからデータを取得して表示#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
// ユーザー一覧を取得して表示する実践例
async function displayUsers() {
const container = document.getElementById("user-list");
const errorMessage = document.getElementById("error-message");
try {
// ローディング表示
container.innerHTML = "<p>読み込み中...</p>";
// APIからユーザー一覧を取得
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error(`HTTPエラー: ${response.status}`);
}
const users = await response.json();
// HTMLを生成して表示
const html = users
.map(
(user) => `
<div class="user-card">
<h3>${user.name}</h3>
<p>Email: ${user.email}</p>
<p>会社: ${user.company.name}</p>
</div>
`
)
.join("");
container.innerHTML = html;
} catch (error) {
errorMessage.textContent = `データの取得に失敗しました: ${error.message}`;
container.innerHTML = "";
}
}
// ページ読み込み時に実行
document.addEventListener("DOMContentLoaded", displayUsers);
|
フォーム送信処理#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
async function handleFormSubmit(event) {
event.preventDefault();
const form = event.target;
const submitButton = form.querySelector('button[type="submit"]');
const formData = new FormData(form);
// ボタンを無効化
submitButton.disabled = true;
submitButton.textContent = "送信中...";
try {
const response = await fetch("/api/contact", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(Object.fromEntries(formData)),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || "送信に失敗しました");
}
const result = await response.json();
// 成功メッセージを表示
alert("送信が完了しました!");
form.reset();
} catch (error) {
alert(`エラー: ${error.message}`);
} finally {
// ボタンを有効化
submitButton.disabled = false;
submitButton.textContent = "送信";
}
}
document.getElementById("contact-form").addEventListener("submit", handleFormSubmit);
|
データのキャッシュ処理#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
// 簡易的なキャッシュ機能付きのデータ取得
const cache = new Map();
async function fetchWithCache(url, cacheTimeMs = 60000) {
const cached = cache.get(url);
// キャッシュが有効な場合はキャッシュを返す
if (cached && Date.now() - cached.timestamp < cacheTimeMs) {
console.log("キャッシュからデータを取得");
return cached.data;
}
console.log("APIからデータを取得");
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
// キャッシュに保存
cache.set(url, {
data,
timestamp: Date.now(),
});
return data;
}
// 使用例
async function loadProducts() {
const products = await fetchWithCache("https://api.example.com/products");
console.log(products);
}
|
ループ内でのasync/await#
配列の各要素に対して非同期処理を行う場合の注意点です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
// 良い例: for...of を使った逐次処理
async function processItemsSequentially(items) {
const results = [];
for (const item of items) {
const result = await processItem(item); // 1つずつ順番に処理
results.push(result);
}
return results;
}
// 良い例: Promise.all を使った並列処理
async function processItemsInParallel(items) {
const results = await Promise.all(
items.map((item) => processItem(item)) // すべて同時に処理
);
return results;
}
// 悪い例: forEachはawaitを正しく待たない
async function processItemsBad(items) {
const results = [];
// forEachはasync関数を待たない
items.forEach(async (item) => {
const result = await processItem(item);
results.push(result); // 順序が保証されない
});
console.log(results); // 空の配列が出力される可能性
}
|
よくある間違いと注意点#
1. awaitの忘れ#
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 間違い: awaitを忘れている
async function fetchData() {
const response = fetch("https://api.example.com/data"); // awaitがない
const data = response.json(); // responseはPromiseオブジェクト
return data;
}
// 正しい
async function fetchData() {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
return data;
}
|
2. async関数の戻り値はPromise#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 間違い: async関数の戻り値を直接使おうとしている
async function getData() {
return { name: "太郎" };
}
const data = getData();
console.log(data.name); // undefined(dataはPromiseオブジェクト)
// 正しい
const data = await getData(); // async関数内で使用
console.log(data.name); // "太郎"
// または
getData().then((data) => {
console.log(data.name); // "太郎"
});
|
3. 不要な逐次実行#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 非効率: 独立した処理を順番に実行している
async function fetchAllData() {
const users = await fetchUsers(); // 1秒
const products = await fetchProducts(); // 1秒
const orders = await fetchOrders(); // 1秒
// 合計3秒かかる
}
// 効率的: 並列で実行
async function fetchAllData() {
const [users, products, orders] = await Promise.all([
fetchUsers(),
fetchProducts(),
fetchOrders(),
]);
// 約1秒で完了
}
|
4. エラーハンドリングの忘れ#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 危険: エラーハンドリングがない
async function riskyFunction() {
const data = await fetch("https://api.example.com/data");
return data.json();
}
// 安全: try-catchでエラーを処理
async function safeFunction() {
try {
const response = await fetch("https://api.example.com/data");
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("エラー:", error);
return null;
}
}
|
async/awaitを使うべき場面#
async/awaitは以下のような場面で特に効果を発揮します。
| 場面 |
理由 |
| 複数の非同期処理を順番に実行する場合 |
Promiseチェーンよりも読みやすい |
| 条件分岐を含む非同期処理 |
通常のif文で書ける |
| ループ内での非同期処理 |
for…ofと組み合わせて直感的に書ける |
| エラーハンドリングが複雑な場合 |
try-catchで統一的に処理できる |
| デバッグが必要な場合 |
スタックトレースが追いやすい |
まとめ#
async/awaitは、JavaScriptの非同期処理を直感的に書くための強力な構文です。本記事で解説した内容をまとめます。
async関数は必ずPromiseを返す
awaitはPromiseの完了を待ち、結果を取得する
- エラー処理は
try-catchで行う
- 独立した複数の非同期処理は
Promise.allで並列実行する
forEach内でのawaitは期待通りに動作しないため、for...ofを使う
async/awaitをマスターすれば、API通信やファイル読み込みなど、さまざまな非同期処理を効率的に記述できるようになります。まずは本記事のコード例を実際に動かして、感覚をつかんでみてください。
参考リンク#