「非同期処理ってなんだか難しい……」そう感じたことはありませんか?
JavaScriptで開発をしていると、「順番に実行されない」「データ取得後の処理がうまくつながらない」など、非同期処理によるトラブルに直面することは多いものです。特にPromise
やasync/await
の使い方を間違えると、処理の順序が意図とズレてしまい、思わぬバグを引き起こすことも。
本来、非同期処理はうまく使えばコードの効率や可読性を高めてくれる強力な機能です。しかし、「同期的に見せたいのに非同期のまま暴走する」「forEach内でawaitが効かない」といった具体的な壁にぶつかると、調べるにも何をキーワードにすればいいかすら迷ってしまいますよね。
この記事では、「Promiseを使って同期処理のように順番に実行したい」悩みを解決すべく、基礎から応用まで幅広く解説します。初心者の方はもちろん、業務で使いこなしたい中級者にも役立つ内容です。
「ただ動くコード」ではなく、「意図通りに動く読みやすいコード」を書くために、Promiseの力を正しく使いこなしていきましょう!
同期処理・非同期処理の基本|Promiseを学ぶ前に知っておくべきこと
JavaScriptにおける同期処理と非同期処理の違い
JavaScriptの処理には同期処理と非同期処理の2つの方式があります。この違いを理解することが、Promiseやasync/awaitを使いこなす第一歩です。
同期処理:上から順番に実行される処理
同期処理は、コードが書かれた順番通りに実行され、一つの処理が完了してから次の処理に進む方式です。
console.log('処理開始');
// 重い処理をシミュレート(実際には推奨されない書き方)
function heavyTask() {
const start = Date.now();
while (Date.now() - start < 3000) {
// 3秒間ブロック
}
return '重い処理完了';
}
console.log('重い処理を開始');
const result = heavyTask();
console.log(result);
console.log('処理終了');
// 実行結果:
// 処理開始
// 重い処理を開始
// (3秒待機)
// 重い処理完了
// 処理終了
非同期処理:完了を待たずに次の処理に進む
非同期処理は、時間のかかる処理(API通信、ファイル読み込みなど)を実行している間も、他の処理を並行して実行できる方式です。
console.log('処理開始');
// 非同期処理(setTimeout)
setTimeout(() => {
console.log('3秒後の処理完了');
}, 3000);
console.log('処理終了');
// 実行結果:
// 処理開始
// 処理終了
// (3秒後)
// 3秒後の処理完了
同期処理と非同期処理の比較表
項目 | 同期処理 | 非同期処理 |
---|---|---|
実行順序 | 上から順番に実行 | 完了を待たずに次の処理へ |
ブロッキング | 処理が完了するまで次に進まない | 他の処理と並行実行可能 |
ユーザー体験 | 重い処理中は画面が固まる | 画面が固まらずスムーズ |
適用場面 | 計算処理、変数操作など | API通信、ファイル操作など |
コードの複雑さ | シンプルで理解しやすい | コールバックやPromiseが必要 |
コールバック・Promise・async/awaitの構文と比較
JavaScript の非同期処理は歴史的に進化してきました。それぞれの特徴を理解することで、適切な手法を選択できるようになります。
1. コールバック:非同期処理の原始的な書き方
// コールバック地獄の例
function fetchUserData(userId, callback) {
setTimeout(() => {
const userData = { id: userId, name: 'ユーザー名' };
callback(null, userData);
}, 1000);
}
function fetchUserPosts(userId, callback) {
setTimeout(() => {
const posts = ['投稿1', '投稿2'];
callback(null, posts);
}, 1000);
}
// ネストが深くなり、読みにくい
fetchUserData(1, (error, user) => {
if (error) {
console.error('ユーザー取得エラー:', error);
return;
}
fetchUserPosts(user.id, (error, posts) => {
if (error) {
console.error('投稿取得エラー:', error);
return;
}
console.log('ユーザー:', user);
console.log('投稿:', posts);
});
});
2. Promise:コールバック地獄を解決
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const userData = { id: userId, name: 'ユーザー名' };
resolve(userData);
}, 1000);
});
}
function fetchUserPosts(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const posts = ['投稿1', '投稿2'];
resolve(posts);
}, 1000);
});
}
// Promiseチェーンで可読性向上
fetchUserData(1)
.then(user => {
console.log('ユーザー:', user);
return fetchUserPosts(user.id);
})
.then(posts => {
console.log('投稿:', posts);
})
.catch(error => {
console.error('エラー:', error);
});
3. async/await:同期処理のような書き方を実現
async function getUserDataAndPosts(userId) {
try {
const user = await fetchUserData(userId);
console.log('ユーザー:', user);
const posts = await fetchUserPosts(user.id);
console.log('投稿:', posts);
return { user, posts };
} catch (error) {
console.error('エラー:', error);
}
}
// 関数の呼び出し
getUserDataAndPosts(1);
各手法の比較表
手法 | 可読性 | エラーハンドリング | 学習コスト | デバッグ性 |
---|---|---|---|---|
コールバック | ❌ 低い | ❌ 複雑 | ⭕ 低い | ❌ 困難 |
Promise | ⭕ 良い | ⭕ .catch() で統一 | 🔶 中程度 | ⭕ 良い |
async/await | ⭐ 最高 | ⭐ try-catch で直感的 | 🔶 中程度 | ⭐ 最高 |
イベントループと非同期処理の実行タイミングの理解
JavaScriptがどのように非同期処理を実行しているかを理解することで、コードの動作を予測しやすくなります。
JavaScriptエンジンの構成要素
┌─────────────────┐ ┌─────────────────┐
│ コールスタック │ │ Web API │
│ │ │ ・setTimeout │
│ function main() │────▶│ ・fetch │
│ function foo() │ │ ・DOM Events │
└─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐
│ イベントループ │◀───│ コールバックキュー │
│ │ │ │
│(常に監視中) │ │ 実行待ちの関数 │
└─────────────────┘ └─────────────────┘
実行順序の具体例
console.log('1: 同期処理開始');
setTimeout(() => {
console.log('3: setTimeout完了');
}, 0);
Promise.resolve().then(() => {
console.log('2: Promise完了');
});
console.log('1: 同期処理終了');
// 実行結果:
// 1: 同期処理開始
// 1: 同期処理終了
// 2: Promise完了
// 3: setTimeout完了
なぜこの順序になるのか?
- 同期処理が最優先:
console.log
は即座に実行される - マイクロタスク(Promise)が次:
Promise.then()
は優先度が高い - マクロタスク(setTimeout)が最後:
setTimeout
は優先度が低い
実際のAPI通信での動作例
async function demonstrateExecutionOrder() {
console.log('処理開始');
// 非同期処理を開始(awaitなし)
const promise1 = fetch('/api/users');
const promise2 = fetch('/api/posts');
console.log('API通信開始(並行実行)');
// 両方の完了を待つ
const [users, posts] = await Promise.all([promise1, promise2]);
console.log('すべてのAPI通信完了');
return { users, posts };
}
この基礎知識を押さえることで、次のセクションでPromiseとasync/awaitを使った実践的な同期処理の実装に入ることができます。
Promiseとasync/awaitの使い分け・同期的な書き方を実現する方法
Promiseチェーンで処理を順番に実行する方法
Promiseチェーンを使うことで、複数の非同期処理を順番に実行し、同期処理のような流れを作ることができます。重要なのはreturn文を適切に使うことです。
基本的なPromiseチェーンの書き方
// API通信をシミュレートする関数
function fetchUser(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: userId, name: `ユーザー${userId}` });
}, 1000);
});
}
function fetchUserPosts(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([`${userId}の投稿1`, `${userId}の投稿2`]);
}, 1000);
});
}
function updateUI(user, posts) {
return new Promise((resolve) => {
setTimeout(() => {
console.log('UI更新完了:', { user, posts });
resolve('UI更新完了');
}, 500);
});
}
// ❌ 間違った書き方:returnを忘れる
fetchUser(1)
.then(user => {
console.log('ユーザー取得:', user);
fetchUserPosts(user.id); // ❌ returnがない!
})
.then(posts => {
console.log('投稿:', posts); // undefined になる
});
// ⭕ 正しい書き方:必ずreturnする
fetchUser(1)
.then(user => {
console.log('ユーザー取得:', user);
return fetchUserPosts(user.id); // ⭕ returnを忘れずに
})
.then(posts => {
console.log('投稿取得:', posts);
return updateUI({ id: 1, name: 'ユーザー1' }, posts);
})
.then(result => {
console.log('処理完了:', result);
})
.catch(error => {
console.error('エラー発生:', error);
});
実際のfetchを使った例
// 実際のAPI通信での例
function fetchUserDataStep() {
return fetch('/api/user/1')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return response.json(); // ⭕ 必ずreturn
})
.then(user => {
console.log('ユーザー情報:', user);
return fetch(`/api/users/${user.id}/posts`); // ⭕ 次のPromiseをreturn
})
.then(response => response.json())
.then(posts => {
console.log('投稿一覧:', posts);
return { user: { id: 1 }, posts }; // ⭕ 最終結果をreturn
})
.catch(error => {
console.error('処理中にエラー:', error);
throw error; // エラーを再スローして呼び出し元に伝える
});
}
// 使用例
fetchUserDataStep()
.then(result => {
console.log('すべての処理完了:', result);
})
.catch(error => {
console.log('最終的なエラーハンドリング:', error.message);
});
Promiseチェーンのreturn規則
returnする値 | 次の.then()が受け取る値 |
---|---|
Promise | そのPromiseの解決値 |
値 | その値そのもの |
何もreturnしない | undefined |
throw Error | .catch() に渡される |
async/awaitの使い方・同期的記述による可読性向上テクニック
async/awaitは、Promiseを同期処理のように書けるシンタックスシュガーです。可読性が大幅に向上し、エラーハンドリングも直感的になります。
基本的なasync/awaitの書き方
// 上記のPromiseチェーンをasync/awaitで書き換え
async function fetchUserDataAsync() {
try {
// 1. ユーザー情報を取得
const userResponse = await fetch('/api/user/1');
if (!userResponse.ok) {
throw new Error(`HTTP Error: ${userResponse.status}`);
}
const user = await userResponse.json();
console.log('ユーザー情報:', user);
// 2. 投稿情報を取得
const postsResponse = await fetch(`/api/users/${user.id}/posts`);
const posts = await postsResponse.json();
console.log('投稿一覧:', posts);
// 3. 結果を返す
return { user, posts };
} catch (error) {
console.error('処理中にエラー:', error);
throw error;
}
}
// 使用例
async function main() {
try {
const result = await fetchUserDataAsync();
console.log('すべての処理完了:', result);
} catch (error) {
console.log('最終的なエラーハンドリング:', error.message);
}
}
main();
async/awaitの重要なポイント
// ❌ よくある間違い:asyncをつけ忘れる
function incorrectFunction() {
const result = await fetch('/api/data'); // ❌ SyntaxError!
return result;
}
// ⭕ 正しい書き方:async functionにする
async function correctFunction() {
const result = await fetch('/api/data'); // ⭕ OK
return result;
}
// ❌ よくある間違い:awaitをつけ忘れる
async function forgotAwait() {
const promise = fetch('/api/data'); // ❌ Promiseオブジェクトが返る
console.log(promise); // Promise<pending>
return promise;
}
// ⭕ 正しい書き方:awaitで解決値を取得
async function correctAwait() {
const response = await fetch('/api/data'); // ⭕ Response オブジェクトが返る
const data = await response.json(); // ⭕ 実際のデータが返る
console.log(data);
return data;
}
PromiseチェーンとAsync/Awaitの比較
// Promiseチェーン版
function promiseVersion() {
return fetch('/api/user')
.then(response => response.json())
.then(user => {
return fetch(`/api/users/${user.id}/profile`)
.then(profileResponse => profileResponse.json())
.then(profile => {
return { user, profile };
});
})
.catch(error => {
console.error('エラー:', error);
throw error;
});
}
// Async/Await版(同じ処理)
async function asyncVersion() {
try {
const response = await fetch('/api/user');
const user = await response.json();
const profileResponse = await fetch(`/api/users/${user.id}/profile`);
const profile = await profileResponse.json();
return { user, profile };
} catch (error) {
console.error('エラー:', error);
throw error;
}
}
fetch/axiosによるAPI逐次実行とデータ加工・UI更新の流れ
実務でよくある「データ取得→加工→UI更新」の流れを、具体的な例で解説します。
fetchを使った実践例
// DOM要素の取得
const userInfoElement = document.getElementById('user-info');
const postsListElement = document.getElementById('posts-list');
const loadingElement = document.getElementById('loading');
async function loadUserDashboard(userId) {
try {
// ローディング表示
loadingElement.style.display = 'block';
userInfoElement.innerHTML = '';
postsListElement.innerHTML = '';
// 1. ユーザー基本情報を取得
console.log('ユーザー情報を取得中...');
const userResponse = await fetch(`/api/users/${userId}`);
if (!userResponse.ok) {
throw new Error(`ユーザー取得失敗: ${userResponse.status}`);
}
const user = await userResponse.json();
console.log('ユーザー情報取得完了:', user);
// 2. ユーザー情報をDOMに反映
userInfoElement.innerHTML = `
<h2>${user.name}</h2>
<p>Email: ${user.email}</p>
<p>登録日: ${new Date(user.createdAt).toLocaleDateString()}</p>
`;
// 3. 取得したユーザーIDで投稿一覧を取得
console.log('投稿一覧を取得中...');
const postsResponse = await fetch(`/api/users/${user.id}/posts?limit=10`);
if (!postsResponse.ok) {
throw new Error(`投稿取得失敗: ${postsResponse.status}`);
}
const posts = await postsResponse.json();
console.log('投稿一覧取得完了:', posts);
// 4. 投稿データを加工(日付順ソート、フィルタリング)
const processedPosts = posts
.filter(post => post.published) // 公開済みのみ
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) // 新しい順
.slice(0, 5); // 最新5件
// 5. 投稿一覧をDOMに反映
postsListElement.innerHTML = processedPosts
.map(post => `
<div class="post-item">
<h3>${post.title}</h3>
<p>${post.content.substring(0, 100)}...</p>
<small>投稿日: ${new Date(post.createdAt).toLocaleDateString()}</small>
</div>
`)
.join('');
// 6. 追加のメタ情報を取得(投稿数、フォロワー数など)
console.log('統計情報を取得中...');
const statsResponse = await fetch(`/api/users/${user.id}/stats`);
const stats = await statsResponse.json();
// 7. 統計情報を既存のユーザー情報に追加
userInfoElement.innerHTML += `
<div class="user-stats">
<span>投稿数: ${stats.postCount}</span>
<span>フォロワー: ${stats.followerCount}</span>
</div>
`;
console.log('すべての処理完了');
return { user, posts: processedPosts, stats };
} catch (error) {
console.error('ダッシュボード読み込みエラー:', error);
// エラー時のUI更新
userInfoElement.innerHTML = `
<div class="error-message">
<p>データの読み込みに失敗しました</p>
<p>エラー: ${error.message}</p>
<button onclick="loadUserDashboard(${userId})">再試行</button>
</div>
`;
throw error;
} finally {
// 必ずローディングを非表示にする
loadingElement.style.display = 'none';
}
}
// 使用例
loadUserDashboard(123)
.then(result => {
console.log('ダッシュボード読み込み成功:', result);
})
.catch(error => {
console.log('最終エラーハンドリング:', error.message);
});
axiosを使った場合の例
// axios使用時(より簡潔になる)
async function loadUserDashboardWithAxios(userId) {
try {
loadingElement.style.display = 'block';
// 1. ユーザー情報取得(axiosは自動でJSONパース)
const { data: user } = await axios.get(`/api/users/${userId}`);
console.log('ユーザー情報:', user);
// 2. DOM更新
updateUserInfo(user);
// 3. 投稿一覧取得
const { data: posts } = await axios.get(`/api/users/${user.id}/posts`, {
params: { limit: 10 }
});
// 4. データ加工とDOM更新
const processedPosts = processPostsData(posts);
updatePostsList(processedPosts);
// 5. 統計情報取得
const { data: stats } = await axios.get(`/api/users/${user.id}/stats`);
updateUserStats(stats);
return { user, posts: processedPosts, stats };
} catch (error) {
// axiosはレスポンスエラーを自動で判定
const errorMessage = error.response?.data?.message || error.message;
console.error('エラー:', errorMessage);
showErrorMessage(errorMessage);
throw error;
} finally {
loadingElement.style.display = 'none';
}
}
// ヘルパー関数
function updateUserInfo(user) {
userInfoElement.innerHTML = `
<h2>${user.name}</h2>
<p>Email: ${user.email}</p>
`;
}
function processPostsData(posts) {
return posts
.filter(post => post.published)
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
.slice(0, 5);
}
function updatePostsList(posts) {
postsListElement.innerHTML = posts
.map(post => `<div class="post-item">${post.title}</div>`)
.join('');
}
function updateUserStats(stats) {
userInfoElement.innerHTML += `
<div class="user-stats">
投稿数: ${stats.postCount} | フォロワー: ${stats.followerCount}
</div>
`;
}
function showErrorMessage(message) {
userInfoElement.innerHTML = `
<div class="error-message">
エラー: ${message}
<button onclick="retry()">再試行</button>
</div>
`;
}
エラーハンドリングのベストプラクティス
async function robustApiCall(url, options = {}) {
const maxRetries = 3;
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`API呼び出し試行 ${attempt}/${maxRetries}: ${url}`);
const response = await fetch(url, {
timeout: 10000, // 10秒でタイムアウト
...options
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log(`API呼び出し成功 (試行${attempt}回目)`);
return data;
} catch (error) {
lastError = error;
console.warn(`試行${attempt}回目失敗:`, error.message);
// 最後の試行でなければ少し待ってからリトライ
if (attempt < maxRetries) {
const waitTime = attempt * 1000; // 1秒, 2秒, 3秒...と増加
console.log(`${waitTime}ms後にリトライします...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
}
// すべての試行が失敗した場合
throw new Error(`API呼び出しが${maxRetries}回失敗しました: ${lastError.message}`);
}
// 使用例
async function fetchWithRetry() {
try {
const userData = await robustApiCall('/api/user/1');
const postsData = await robustApiCall(`/api/users/${userData.id}/posts`);
return { user: userData, posts: postsData };
} catch (error) {
console.error('すべてのリトライが失敗:', error.message);
throw error;
}
}
このセクションでは、Promiseチェーンとasync/awaitの実践的な使い方を、実際のAPI通信を想定した具体例で詳しく解説しました。次のセクションでは、より高度な応用テクニックについて説明します。
実務で役立つ非同期処理の応用テクニック集
React / Next.jsにおける非同期処理の設計と実装例
React環境での非同期処理には特有の注意点があります。コンポーネントのライフサイクルやState管理との連携を適切に行う必要があります。
ReactのuseEffect内でのasync/await使用法
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// ❌ 間違った書き方:useEffectを直接asyncにする
// useEffect(async () => {
// const userData = await fetchUser(userId); // これはダメ!
// }, [userId]);
// ⭕ 正しい書き方:useEffect内で async 関数を定義・呼び出し
useEffect(() => {
let isCancelled = false; // クリーンアップ用フラグ
async function loadUserData() {
try {
setLoading(true);
setError(null);
// 1. ユーザー情報を取得
const userResponse = await fetch(`/api/users/${userId}`);
if (!userResponse.ok) {
throw new Error(`User fetch failed: ${userResponse.status}`);
}
const userData = await userResponse.json();
// コンポーネントがアンマウントされていないかチェック
if (isCancelled) return;
setUser(userData);
// 2. 投稿一覧を取得
const postsResponse = await fetch(`/api/users/${userId}/posts`);
if (!postsResponse.ok) {
throw new Error(`Posts fetch failed: ${postsResponse.status}`);
}
const postsData = await postsResponse.json();
if (isCancelled) return;
setPosts(postsData);
} catch (err) {
if (!isCancelled) {
setError(err.message);
console.error('データ取得エラー:', err);
}
} finally {
if (!isCancelled) {
setLoading(false);
}
}
}
loadUserData();
// クリーンアップ関数:コンポーネントアンマウント時の処理
return () => {
isCancelled = true;
};
}, [userId]); // userIdが変更された時に再実行
// ローディング状態の表示
if (loading) {
return <div className="loading">ユーザー情報を読み込み中...</div>;
}
// エラー状態の表示
if (error) {
return (
<div className="error">
<p>エラーが発生しました: {error}</p>
<button onClick={() => window.location.reload()}>
再読み込み
</button>
</div>
);
}
// 正常データの表示
return (
<div className="user-profile">
<h2>{user?.name}</h2>
<p>Email: {user?.email}</p>
<div className="posts">
<h3>投稿一覧 ({posts.length}件)</h3>
{posts.map(post => (
<div key={post.id} className="post-item">
<h4>{post.title}</h4>
<p>{post.content.substring(0, 100)}...</p>
</div>
))}
</div>
</div>
);
}
export default UserProfile;
カスタムフックを使った再利用可能な非同期処理
// hooks/useAsyncData.js
import { useState, useEffect } from 'react';
function useAsyncData(asyncFunction, dependencies = []) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isCancelled = false;
async function executeAsync() {
try {
setLoading(true);
setError(null);
const result = await asyncFunction();
if (!isCancelled) {
setData(result);
}
} catch (err) {
if (!isCancelled) {
setError(err);
}
} finally {
if (!isCancelled) {
setLoading(false);
}
}
}
executeAsync();
return () => {
isCancelled = true;
};
}, dependencies);
const refetch = () => {
setLoading(true);
setError(null);
// 依存配列を変更せずに再実行するためのトリック
// 実際にはReact Queryなどのライブラリを使うことを推奨
};
return { data, loading, error, refetch };
}
// 使用例
function UserList() {
const { data: users, loading, error } = useAsyncData(
() => fetch('/api/users').then(res => res.json()),
[] // 依存配列が空なので初回のみ実行
);
if (loading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error.message}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Next.jsのApp Routerでのサーバーコンポーネント
// app/users/[id]/page.js(Next.js 13+ App Router)
async function UserPage({ params }) {
const { id } = params;
// サーバーコンポーネントでは直接asyncにできる
async function getUserData() {
try {
// サーバーサイドでのAPI呼び出し
const [userRes, postsRes] = await Promise.all([
fetch(`${process.env.API_BASE_URL}/users/${id}`, {
cache: 'force-cache', // キャッシュ戦略を指定
}),
fetch(`${process.env.API_BASE_URL}/users/${id}/posts`, {
next: { revalidate: 60 }, // 60秒後に再検証
})
]);
if (!userRes.ok || !postsRes.ok) {
throw new Error('データ取得に失敗しました');
}
const [user, posts] = await Promise.all([
userRes.json(),
postsRes.json()
]);
return { user, posts };
} catch (error) {
console.error('サーバーサイドエラー:', error);
throw error;
}
}
try {
const { user, posts } = await getUserData();
return (
<div className="user-page">
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
<div className="posts-section">
<h2>投稿一覧</h2>
{posts.map(post => (
<article key={post.id} className="post">
<h3>{post.title}</h3>
<p>{post.content}</p>
<time>{new Date(post.createdAt).toLocaleDateString()}</time>
</article>
))}
</div>
</div>
);
} catch (error) {
return (
<div className="error-page">
<h1>エラーが発生しました</h1>
<p>{error.message}</p>
</div>
);
}
}
export default UserPage;
ファイルアップロードなど「処理が終わるまで待つ」実装パターン
ファイルアップロードや画像処理など、時間のかかる処理を順次実行するパターンを解説します。
複数ファイルの順次アップロード
async function uploadFilesSequentially(files, onProgress) {
const results = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
try {
console.log(`ファイル ${i + 1}/${files.length} をアップロード中: ${file.name}`);
// プログレス更新
onProgress?.({
current: i + 1,
total: files.length,
fileName: file.name,
status: 'uploading'
});
// 1つずつ順番にアップロード
const result = await uploadSingleFile(file);
results.push(result);
console.log(`アップロード完了: ${file.name}`);
// 成功時のプログレス更新
onProgress?.({
current: i + 1,
total: files.length,
fileName: file.name,
status: 'completed',
result
});
} catch (error) {
console.error(`アップロード失敗: ${file.name}`, error);
// エラー時のプログレス更新
onProgress?.({
current: i + 1,
total: files.length,
fileName: file.name,
status: 'error',
error: error.message
});
// エラーが発生してもアップロードを継続する場合
results.push({
fileName: file.name,
error: error.message,
success: false
});
// エラーで中断する場合は以下のコメントアウトを外す
// throw error;
}
}
return results;
}
async function uploadSingleFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('uploadedAt', new Date().toISOString());
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
// Content-Typeはブラウザが自動設定するので指定しない
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
}
const result = await response.json();
return result;
}
// 使用例(React)
function FileUploader() {
const [files, setFiles] = useState([]);
const [uploadProgress, setUploadProgress] = useState(null);
const [isUploading, setIsUploading] = useState(false);
const handleFileSelect = (event) => {
const selectedFiles = Array.from(event.target.files);
setFiles(selectedFiles);
};
const handleUpload = async () => {
if (files.length === 0) return;
setIsUploading(true);
setUploadProgress({ current: 0, total: files.length });
try {
const results = await uploadFilesSequentially(files, (progress) => {
setUploadProgress(progress);
});
console.log('すべてのアップロード完了:', results);
alert('アップロードが完了しました!');
// 成功時はファイル選択をリセット
setFiles([]);
} catch (error) {
console.error('アップロードエラー:', error);
alert(`アップロードエラー: ${error.message}`);
} finally {
setIsUploading(false);
setUploadProgress(null);
}
};
return (
<div className="file-uploader">
<input
type="file"
multiple
onChange={handleFileSelect}
disabled={isUploading}
/>
{files.length > 0 && (
<div className="file-list">
<p>選択されたファイル: {files.length}個</p>
<ul>
{files.map((file, index) => (
<li key={index}>
{file.name} ({(file.size / 1024 / 1024).toFixed(2)} MB)
</li>
))}
</ul>
</div>
)}
<button
onClick={handleUpload}
disabled={isUploading || files.length === 0}
>
{isUploading ? 'アップロード中...' : 'アップロード開始'}
</button>
{uploadProgress && (
<div className="upload-progress">
<p>
進行状況: {uploadProgress.current}/{uploadProgress.total}
</p>
<p>現在のファイル: {uploadProgress.fileName}</p>
<p>ステータス: {uploadProgress.status}</p>
<div className="progress-bar">
<div
className="progress-fill"
style={{
width: `${(uploadProgress.current / uploadProgress.total) * 100}%`
}}
/>
</div>
</div>
)}
</div>
);
}
画像リサイズ処理の順次実行
// 画像をリサイズしてからアップロードする例
async function processAndUploadImages(imageFiles, targetWidth = 800) {
const processedResults = [];
for (const file of imageFiles) {
try {
console.log(`画像処理開始: ${file.name}`);
// 1. 画像をリサイズ
const resizedBlob = await resizeImage(file, targetWidth);
// 2. リサイズした画像をアップロード
const uploadResult = await uploadImageBlob(resizedBlob, file.name);
processedResults.push({
originalFile: file.name,
originalSize: file.size,
processedSize: resizedBlob.size,
uploadResult
});
console.log(`処理完了: ${file.name}`);
} catch (error) {
console.error(`処理失敗: ${file.name}`, error);
processedResults.push({
originalFile: file.name,
error: error.message
});
}
}
return processedResults;
}
function resizeImage(file, targetWidth) {
return new Promise((resolve, reject) => {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
img.onload = () => {
// アスペクト比を保持してリサイズ
const aspectRatio = img.height / img.width;
canvas.width = targetWidth;
canvas.height = targetWidth * aspectRatio;
// 画像を描画
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// Blobとして出力
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error('画像の変換に失敗しました'));
}
}, 'image/jpeg', 0.8); // JPEG, 品質80%
};
img.onerror = () => {
reject(new Error('画像の読み込みに失敗しました'));
};
img.src = URL.createObjectURL(file);
});
}
async function uploadImageBlob(blob, originalFileName) {
const formData = new FormData();
formData.append('image', blob, `resized_${originalFileName}`);
const response = await fetch('/api/upload-image', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`画像アップロード失敗: ${response.status}`);
}
return await response.json();
}
Promise.allとasync/awaitの使い分け
// ❌ 順次実行(遅い):前の処理が終わってから次を開始
async function slowSequentialProcessing(urls) {
const results = [];
for (const url of urls) {
const response = await fetch(url); // 1つずつ順番に実行
const data = await response.json();
results.push(data);
}
return results;
}
// ⭕ 並列実行(速い):すべて同時に開始
async function fastParallelProcessing(urls) {
const promises = urls.map(url =>
fetch(url).then(response => response.json())
);
const results = await Promise.all(promises);
return results;
}
// 🔶 制御された並列実行:同時実行数を制限
async function controlledParallelProcessing(urls, concurrency = 3) {
const results = [];
for (let i = 0; i < urls.length; i += concurrency) {
const batch = urls.slice(i, i + concurrency);
const batchPromises = batch.map(url =>
fetch(url).then(response => response.json())
);
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
console.log(`バッチ ${Math.ceil((i + 1) / concurrency)} 完了`);
}
return results;
}
// 使い分けの例
async function demonstrateProcessingMethods() {
const urls = [
'/api/data1',
'/api/data2',
'/api/data3',
'/api/data4'
];
console.time('順次実行');
await slowSequentialProcessing(urls);
console.timeEnd('順次実行'); // 例: 4000ms
console.time('並列実行');
await fastParallelProcessing(urls);
console.timeEnd('並列実行'); // 例: 1000ms
console.time('制御並列実行');
await controlledParallelProcessing(urls, 2);
console.timeEnd('制御並列実行'); // 例: 2000ms
}
実務現場でよくある非同期処理の落とし穴・バグの回避・デバッグ事例
実際の開発現場でよく遭遇する非同期処理のバグと、その対策を解説します。
よくある落とし穴1:awaitの付け忘れ
// ❌ バグのあるコード
async function fetchUserDataBuggy(userId) {
const response = fetch(`/api/users/${userId}`); // awaitを忘れた!
const user = response.json(); // Promiseオブジェクトになってしまう
console.log(user); // Promise<pending>
return user; // 呼び出し元も期待した値を受け取れない
}
// ⭕ 修正版
async function fetchUserDataFixed(userId) {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
console.log(user); // 実際のユーザーデータ
return user;
}
// デバッグ用のチェック関数
function checkPromise(value, varName) {
if (value instanceof Promise) {
console.warn(`⚠️ ${varName} はPromiseです。awaitを付け忘れていませんか?`);
console.log(`実際の値:`, value);
return true;
}
return false;
}
// 使用例
async function debugExample() {
const response = fetch('/api/users/1'); // わざとawaitを忘れる
if (checkPromise(response, 'response')) {
console.log('修正が必要です!');
}
}
よくある落とし穴2:forEachでawaitを使う
const userIds = [1, 2, 3, 4, 5];
// ❌ 間違った方法:forEachではawaitが効かない
async function fetchUsersWrong(userIds) {
const users = [];
userIds.forEach(async (id) => {
const user = await fetch(`/api/users/${id}`).then(r => r.json());
users.push(user); // タイミングがバラバラになる
});
console.log(users); // 空の配列!(非同期処理が完了前に実行される)
return users;
}
// ⭕ 正しい方法1:for...ofを使用
async function fetchUsersCorrect1(userIds) {
const users = [];
for (const id of userIds) {
const user = await fetch(`/api/users/${id}`).then(r => r.json());
users.push(user);
}
console.log(users); // 期待した配列
return users;
}
// ⭕ 正しい方法2:Promise.allで並列実行
async function fetchUsersCorrect2(userIds) {
const promises = userIds.map(id =>
fetch(`/api/users/${id}`).then(r => r.json())
);
const users = await Promise.all(promises);
console.log(users); // 期待した配列
return users;
}
// 実行時間の比較
async function comparePerformance() {
const userIds = [1, 2, 3, 4, 5];
console.time('順次実行');
await fetchUsersCorrect1(userIds);
console.timeEnd('順次実行');
console.time('並列実行');
await fetchUsersCorrect2(userIds);
console.timeEnd('並列実行');
}
よくある落とし穴3:Promiseチェーンとasync/awaitの混在
// ❌ 混在していて読みにくい
async function mixedStyleBad(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(async (user) => {
const posts = await fetch(`/api/users/${user.id}/posts`)
.then(r => r.json()); // さらに混在
return { user, posts };
})
.catch(error => {
console.error(error);
throw error;
});
}
// ⭕ async/awaitで統一
async function consistentAsyncStyle(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
const postsResponse = await fetch(`/api/users/${user.id}/posts`);
const posts = await postsResponse.json();
return { user, posts };
} catch (error) {
console.error(error);
throw error;
}
}
// ⭕ Promiseチェーンで統一
function consistentPromiseStyle(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(user => {
return fetch(`/api/users/${user.id}/posts`)
.then(postsResponse => postsResponse.json())
.then(posts => ({ user, posts }));
})
.catch(error => {
console.error(error);
throw error;
});
}
デバッグテクニック
// デバッグ用のラッパー関数
function debugAsync(asyncFunction, functionName) {
return async function(...args) {
console.log(`🚀 ${functionName} 開始:`, args);
const startTime = Date.now();
try {
const result = await asyncFunction(...args);
const endTime = Date.now();
console.log(`✅ ${functionName} 成功 (${endTime - startTime}ms):`, result);
return result;
} catch (error) {
const endTime = Date.now();
console.error(`❌ ${functionName} 失敗 (${endTime - startTime}ms):`, error);
throw error;
}
};
}
// 使用例
const debuggedFetch = debugAsync(
async (url) => {
const response = await fetch(url);
return response.json();
},
'API通信'
);
// 実行するとログが出力される
await debuggedFetch('/api/users/1');
// Promise の状態を確認する関数
function inspectPromise(promise, name = 'Promise') {
console.log(`🔍 ${name} の状態を確認中...`);
promise
.then(result => {
console.log(`✅ ${name} resolved:`, result);
})
.catch(error => {
console.error(`❌ ${name} rejected:`, error);
});
// Promise自体も返す(チェーン可能)
return promise;
}
// 使用例
const userPromise = fetch('/api/users/1');
inspectPromise(userPromise, 'ユーザー取得');
// 非同期処理のタイムアウト処理
function withTimeout(promise, timeoutMs, errorMessage) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(errorMessage || `処理がタイムアウトしました (${timeoutMs}ms)`));
}, timeoutMs);
});
return Promise.race([promise, timeoutPromise]);
}
// 使用例
async function fetchWithTimeout() {
try {
const result = await withTimeout(
fetch('/api/slow-endpoint'),
5000,
'API通信がタイムアウトしました'
);
return result.json();
} catch (error) {
console.error('タイムアウトまたはエラー:', error.message);
throw error;
}
}
ブラウザ開発者ツールでのデバッグ
// コンソールでのデバッグ用関数
window.debugAsync = {
// 現在実行中のPromiseを追跡
activePromises: new Set(),
// Promiseを登録して追跡
track(promise, name) {
this.activePromises.add({ promise, name, startTime: Date.now() });
promise.finally(() => {
// 完了したら削除
this.activePromises.forEach(item => {
if (item.promise === promise) {
this.activePromises.delete(item);
}
});
});
return promise;
},
// アクティブなPromiseを表示
showActive() {
console.table([...this.activePromises].map(item => ({
name: item.name,
duration: `${Date.now() - item.startTime}ms`
})));
}
};
// 使用例(ブラウザのコンソールで実行)
/*
// Promise を追跡
const promise1 = debugAsync.track(fetch('/api/users/1'), 'ユーザー取得');
const promise2 = debugAsync.track(fetch('/api/posts'), '投稿取得');
// アクティブなPromiseを確認
debugAsync.showActive();
*/
これらの応用テクニックと落とし穴の回避方法を理解することで、実務での非同期処理をより確実かつ効率的に実装できるようになります。
よくある質問(FAQ)
-
Promise.all
とPromise.allSettled
はどう違う? -
最大の違いは、一つでもエラーが発生した場合の挙動です。
// Promise.all の場合:一つでもrejectされると全体がエラーになる const fetchUserData = async () => { try { const [user, posts, comments] = await Promise.all([ fetch('/api/user/1'), fetch('/api/posts/1'), // これがエラーになると... fetch('/api/comments/1') ]); // エラーが発生すると、ここには到達しない } catch (error) { console.log('いずれかのAPIでエラーが発生:', error); } }; // Promise.allSettled の場合:エラーがあっても全ての結果を取得 const fetchUserDataSafely = async () => { const results = await Promise.allSettled([ fetch('/api/user/1'), fetch('/api/posts/1'), // これがエラーになっても... fetch('/api/comments/1') ]); results.forEach((result, index) => { if (result.status === 'fulfilled') { console.log(`API ${index} 成功:`, result.value); } else { console.log(`API ${index} 失敗:`, result.reason); } }); };
使い分けの目安:
- Promise.all: 全ての処理が成功することが前提で、一つでも失敗したら全体を停止したい場合
- Promise.allSettled: 一部が失敗しても他の結果は取得したい場合
-
async/await
はtry-catch
が必須? -
必須ではありませんが、エラーハンドリングのためには強く推奨されます。
// try-catchなしの場合(非推奨)
const badExample = async () => {
const response = await fetch('/api/user'); // エラーが発生すると未処理のPromiseエラーに
const data = await response.json();
return data;
};
// try-catchありの場合(推奨)
const goodExample = async () => {
try {
const response = await fetch('/api/user');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('データ取得エラー:', error);
// エラー時のフォールバック処理
return null;
}
};
// 関数を呼び出す側でもエラーハンドリング可能
const handleUser = async () => {
try {
const user = await badExample(); // ここでもcatchできる
} catch (error) {
console.error('ユーザー処理エラー:', error);
}
};
-
forEach
でawait
を使ったらダメなのはなぜ? -
forEach
はasync/await
を正しく処理できないため、期待通りに順番に実行されません。const urls = ['/api/user/1', '/api/user/2', '/api/user/3'];
// ❌ 間違った書き方:順番に実行されない
const badExample = async () => {
console.log('開始');
urls.forEach(async (url) => {
const response = await fetch(url); // これらは並行して実行される
console.log(`取得完了: ${url}`);
});
console.log('終了'); // fetchより先に実行される
};
// ✅ 正しい書き方1:for...of を使用
const goodExample1 = async () => {
console.log('開始');
for (const url of urls) {
const response = await fetch(url); // 順番に実行される
console.log(`取得完了: ${url}`);
}
console.log('終了'); // 全てのfetchが完了してから実行される
};
// ✅ 正しい書き方2:map + Promise.all で並行実行
const goodExample2 = async () => {
console.log('開始');
const promises = urls.map(async (url) => {
const response = await fetch(url);
console.log(`取得完了: ${url}`);
return response;
});
await Promise.all(promises);
console.log('終了');
};
-
Promise.resolve()
とPromise.reject()
はいつ使う? -
主にテスト用のモックや、条件によって同期・非同期を切り替えたい場合に使用します。
// テスト用のモック関数
const mockApiCall = (shouldSucceed) => {
if (shouldSucceed) {
return Promise.resolve({ id: 1, name: 'テストユーザー' });
} else {
return Promise.reject(new Error('API呼び出し失敗'));
}
};
// 条件による同期・非同期の切り替え
const getUserData = (useCache = false) => {
if (useCache && cachedData) {
// キャッシュがある場合は即座にPromiseを返す
return Promise.resolve(cachedData);
} else {
// キャッシュがない場合は実際のAPI呼び出し
return fetch('/api/user').then(res => res.json());
}
};
// 使用例
const handleUser = async () => {
try {
const user = await getUserData(true); // キャッシュを使用
console.log(user);
} catch (error) {
console.error(error);
}
};
-
async
関数の戻り値は必ずPromise
になる? -
はい、
async
関数は常にPromise
を返します。// 普通の値を返しても...
async function getValue() {
return 42; // 普通の数値を返している
}
// 実際にはPromiseが返される
console.log(getValue()); // Promise {<fulfilled>: 42}
// 使用する際はawaitが必要
const useValue = async () => {
const result = await getValue(); // 42
console.log(result); // 42
};
// またはthenでも取得可能
getValue().then(result => {
console.log(result); // 42
});
// エラーを投げた場合
async function getError() {
throw new Error('エラー発生');
}
console.log(getError()); // Promise {<rejected>: Error: エラー発生}
-
await
はasync
関数の中でしか使えない? -
基本的にはそうですが、モジュールのトップレベル(Top-level await)では例外的に使用できます。
// ❌ 通常の関数内では使用不可
function normalFunction() {
const result = await fetch('/api/data'); // SyntaxError!
}
// ✅ async関数内では使用可能
async function asyncFunction() {
const result = await fetch('/api/data'); // OK
}
// ✅ ES2022以降:モジュールのトップレベルで使用可能
// main.js(モジュールとして)
const response = await fetch('/api/config');
const config = await response.json();
export { config };
// ✅ IIFE(即座に実行する関数式)でも回避可能
(async () => {
const result = await fetch('/api/data');
console.log(result);
})();
-
Promise
チェーンとasync/await
は混在させても大丈夫? -
技術的には可能ですが、コードの一貫性と可読性のため、避けることを推奨します。
// ❌ 混在した書き方(可読性が悪い)
const mixedExample = async () => {
const user = await fetch('/api/user/1').then(res => res.json());
return fetch('/api/posts')
.then(res => res.json())
.then(async (posts) => {
const processedPosts = await processPostsAsync(posts);
return { user, posts: processedPosts };
});
};
// ✅ async/awaitで統一した書き方
const consistentExample = async () => {
try {
const userResponse = await fetch('/api/user/1');
const user = await userResponse.json();
const postsResponse = await fetch('/api/posts');
const posts = await postsResponse.json();
const processedPosts = await processPostsAsync(posts);
return { user, posts: processedPosts };
} catch (error) {
console.error('データ取得エラー:', error);
throw error;
}
};
-
setTimeout
をasync/await
で使うには? -
setTimeout
はPromise
ベースではないため、Promise
でラップする必要があります。// Promise版のsleep関数を作成
const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms));
};
// 使用例
const delayedExecution = async () => {
console.log('開始');
await sleep(2000); // 2秒待機
console.log('2秒後に実行');
await sleep(1000); // さらに1秒待機
console.log('さらに1秒後に実行');
};
// 実用的な例:APIの連続呼び出しで負荷を軽減
const fetchUsersWithDelay = async (userIds) => {
const users = [];
for (const id of userIds) {
const response = await fetch(`/api/user/${id}`);
const user = await response.json();
users.push(user);
// 次のリクエストまで500ms待機(サーバー負荷軽減)
await sleep(500);
}
return users;
};
まとめ
JavaScriptの非同期処理は、初学者から経験豊富な開発者まで、多くの方が悩みを抱えやすい分野です。しかし、Promise
とasync/await
の仕組みを正しく理解し、適切に使い分けることで、より保守性が高く、読みやすいコードを書けるようになります。
この記事では、同期処理と非同期処理の基本的な違いから始まり、コールバック地獄を解決するPromise
チェーン、そして現代的なasync/await
の書き方まで、段階的に解説してきました。特に、実務でよく遭遇するAPI通信やファイル処理、React/Next.jsでの実装パターンを通じて、理論だけでなく実践的なスキルも身につけていただけたのではないでしょうか。
非同期処理のマスターへの道のりは、まず基本概念をしっかりと理解し、その上で実際のコードを書きながら体験を積むことが重要です。今回ご紹介したコード例は、すべてコピー&ペーストして動作確認していただけるものばかりですので、ぜひご自身の開発環境で試してみてください。
また、実務では単純なAPI呼び出しだけでなく、複数の非同期処理を組み合わせたり、エラー処理を考慮したりと、より複雑な要件に対応する必要があります。そんな時こそ、この記事で学んだ基礎知識が活かされるはずです。
最後に、非同期処理は一度理解しても、新しいライブラリやフレームワークを使うたびに、また違った側面が見えてくる奥深い分野でもあります。今回の記事を出発点として、引き続き学習を続けていただければと思います。
まずは明日から、既存のコールバック関数やPromise
チェーンで書かれたコードを、async/await
でリファクタリングしてみませんか?きっと、コードの可読性が格段に向上し、メンテナンスもしやすくなることを実感していただけるでしょう。あなたの JavaScript スキルアップを心から応援しています!
