はじめに
「コールバック地獄って聞いたことある」
「Promiseは何となくわかるけど、async/awaitって必要?」
「非同期処理って、結局どういうこと?」
JavaScriptで開発してると必ず出てくる非同期処理。今回は「なんで非同期処理が必要なの?」から「async/awaitでどう楽になるの?」まで、丁寧に解説します!
そもそも同期処理と非同期処理って何?
JavaScriptは「シングルスレッド」で動く言語です。つまり、一度に一つの処理しかできません。でも、もしすべての処理を順番に待っていたら、Webアプリはとても遅くなってしまいます。そこで非同期処理が必要になるのです。
同期処理 = レジに並ぶ
スーパーのレジを想像してください:
レジが1つしかなく、店員さんも一人。前のお客さんのレジが終わるまで、次のお客さんはずっと待たなければいけません。
// 同期処理の例
console.log('1人目のお会計'); // 3分かかる
console.log('2人目のお会計'); // 2分かかる
console.log('3人目のお会計'); // 4分かかる
// 合計: 9分かかる
このJavaScriptコードでは、各処理が順番に実行されます。console.logが実行される順番は必ず上から下へ。
もし各処理が時間がかかる処理だった場合、前の処理が終わるまで次の処理は実行されません。これが「同期処理」です。
非同期処理 = ファミレスの注文
ファミレスの厨房を想像してください:
ウェイターは注文を受けたら、厨房に伝えて、すぐに次のお客さんの注文を受けに行きます。料理ができるのを待っている必要はありません。
// 非同期処理の例
注文1を受ける → 料理開始 → そのまま次へ
注文2を受ける → 料理開始 → そのまま次へ
注文3を受ける → 料理開始 → そのまま次へ
// 全部同時進行!早い!
コードで表現すると、「注文を受ける」処理を開始したら、その完了を待たずに次の「注文を受ける」処理を開始します。
これにより、複数の処理が同時並行で進み、全体の処理時間が大幅に短縮されます。これが「非同期処理」です。
なんで非同期処理が必要なの?
JavaScriptのイベントループ
JavaScriptには「イベントループ」という仕組みがあります。これは、ユーザーのクリックやキーボード入力、APIのレスポンスなど、様々なイベントを処理するためのメカニズムです。
もしすべてが同期処理だったら、APIのレスポンスを待っている間、ユーザーは一切操作できなくなってしまいます。
ユーザー体験が最悪になるから
同期処理だけだと...
// ❌ 悪い例(同期処理)
button.addEventListener('click', () => {
// 3秒かかるAPI呼び出し
const data = fetchDataSync(); // この間、画面が固まる!
// やっと動く
console.log(data);
});
結果:
- ボタンを押した瞬間から3秒間、ブラウザが完全に固まります
- スクロールできない、他のボタンを押せない、テキスト入力できない
- ユーザーは「フリーズした?アプリが壊れた?」と不安になります
- 最悪の場合、ユーザーはページを離脱してしまいます
非同期処理なら...
// ✅ 良い例(非同期処理)
button.addEventListener('click', async () => {
// API呼び出し中も画面は動く
const data = await fetchData();
console.log(data);
});
結果:
- ボタンを押した後も、画面の操作が可能です
- ローディングアニメーションを表示できます
- ユーザーは「処理中」であることがわかり、安心して待てます
- 処理中でも他の操作(キャンセルボタンを押すなど)ができます
非同期処理の歴史と進化
1. コールバック時代(昔)
// データ取得 → ユーザー情報取得 → 投稿取得
getData(function(data) {
getUser(data.userId, function(user) {
getPosts(user.id, function(posts) {
console.log(posts);
// どんどん右に行く...これがコールバック地獄
});
});
});
問題点:
- ネストが深くなる: 処理が増えるたびに、コードが右に右にインデントされていきます
- エラー処理が複雑: 各コールバックごとにエラー処理を書く必要があります
- デバッグが困難: どこでエラーが起きたか追いにくく、処理の流れが把握しづらい
- コードの再利用が難しい: ネストしたコールバックを別の場所で使うのが大変
2. Promise時代(ちょっと前)
// Promiseチェーンで書く
getData()
.then(data => getUser(data.userId))
.then(user => getPosts(user.id))
.then(posts => console.log(posts))
.catch(error => console.error(error));
改善点:
- ネストが解消: コールバック地獄から解放され、フラットな構造に
- エラー処理の一元化: .catch()メソッドで、すべてのエラーを一箇所で処理
- チェーン可能: Promiseを連鎖させることで、処理の流れが明確に
- でもまだ問題が...: thenの中でreturnを忘れると意図しない動作になる、コードが横に長くなる
3. async/await時代(今)
// async/awaitで書く
async function fetchUserPosts() {
try {
const data = await getData();
const user = await getUser(data.userId);
const posts = await getPosts(user.id);
console.log(posts);
} catch (error) {
console.error(error);
}
}
最高のポイント:
- 同期処理のような書き方: 上から下へ、順番に処理が流れるように見えます
- 可読性の向上: 誰が見ても処理の流れが一目でわかります
- デバッグが簡単: ブレークポイントを設定して、ステップ実行が可能
- エラー処理が直感的: try/catchで同期処理と同じようにエラー処理
- 学習コストが低い: 同期処理を知っていれば、すぐに理解できます
async/awaitの基本
asyncとは?
「この関数は非同期処理を含みますよ」という宣言
asyncキーワードを関数の前に付けると、その関数は自動的にPromiseを返すようになります。
つまり、async関数の中では、awaitを使って非同期処理を「同期処理のように」書けるようになります。
// asyncを付けると、必ずPromiseを返す関数になる
async function myFunction() {
return 'Hello'; // 自動でPromiseでラップされる
}
// 使う時
myFunction().then(result => console.log(result)); // 'Hello'
awaitとは?
「この処理が終わるまで待って」という命令
awaitはPromiseが解決(resolve)されるまで、その場で処理を一時停止します。
しかし、JavaScriptのイベントループ自体はブロックされないので、他の処理(ユーザーのクリックなど)は継続して処理されます。
async function fetchData() {
// awaitを付けると、Promiseが解決するまで待つ
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data; // dataが取得できてから返す
}
重要なルール:
- awaitは必ずasync関数の中でしか使えません!
- awaitを使うことで、Promiseの結果を直接取得できます
- awaitを使わないと、Promiseオブジェクトそのものが返ってきます
実践的な使い方
1. APIからデータを取得
実際のWebアプリケーションで最もよく使われるパターンです。サーバーからデータを取得し、画面に表示する処理です。
// ユーザー情報を取得する関数
async function getUserInfo(userId) {
try {
// APIを呼ぶ
const response = await fetch(`/api/users/${userId}`);
// レスポンスをチェック
if (!response.ok) {
throw new Error('ユーザーが見つかりません');
}
// JSONに変換
const user = await response.json();
return user;
} catch (error) {
console.error('エラー:', error.message);
return null;
}
}
// 使い方
async function displayUser() {
const user = await getUserInfo(123);
if (user) {
console.log(`名前: ${user.name}`);
}
}
2. 複数の処理を並列実行
複数のAPIを同時に呼び出す必要がある場合、順番に実行すると時間がかかります。
Promise.allを使うことで、すべての非同期処理を同時に開始し、すべてが完了するのを待つことができます。
// ❌ 遅い方法(順番に実行)
async function fetchSequential() {
const user = await fetchUser(); // 1秒
const posts = await fetchPosts(); // 1秒
const comments = await fetchComments(); // 1秒
// 合計: 3秒
}
// ✅ 速い方法(並列実行)
async function fetchParallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
]);
// 合計: 1秒(全部同時に実行)
}
3. エラーハンドリング
ネットワークエラーやサーバーエラーなど、非同期処理では様々なエラーが発生する可能性があります。
適切なエラーハンドリングは、ユーザー体験を大幅に向上させます。
// 個別にエラー処理
async function robustFetch() {
let user, posts;
// ユーザー取得(エラーでも続行)
try {
user = await fetchUser();
} catch (error) {
console.log('ユーザー取得失敗:', error);
user = { name: 'ゲスト' };
}
// 投稿取得(エラーでも続行)
try {
posts = await fetchPosts();
} catch (error) {
console.log('投稿取得失敗:', error);
posts = [];
}
return { user, posts };
}
4. ローディング表示の実装
ReactやVueなどのフレームワークでは、データ取得中にローディング表示を出すのが一般的です。
これにより、ユーザーはアプリケーションが処理中であることを理解できます。
// Reactでの例
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 非同期処理を実行
async function loadUser() {
try {
setLoading(true);
const userData = await fetchUser(userId);
setUser(userData);
} catch (err) {
setError('ユーザー情報の取得に失敗しました');
} finally {
setLoading(false);
}
}
loadUser();
}, [userId]);
if (loading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error}</div>;
if (!user) return null;
return <div>こんにちは、{user.name}さん!</div>;
}
よくある間違いと解決法
1. awaitを忘れる
最もよくあるミスです。awaitを忘れると、Promiseオブジェクトがそのまま返ってきて、
意図しない動作になります。デバッグ時に「Promise {}」という表示を見たら、
まずwaitを忘れている可能性が高いです。
// ❌ 間違い
async function badExample() {
const data = fetch('/api/data'); // awaitを忘れた!
console.log(data); // Promise {<pending>} が表示される
}
// ✅ 正解
async function goodExample() {
const response = await fetch('/api/data');
const data = await response.json();
console.log(data); // 実際のデータが表示される
}
2. forEachでawaitを使う
forEachメソッドは非同期処理を待ちません。これはJavaScriptの仕様で、
forEachのコールバック関数がasyncであっても、その完了を待たずに次のループに進んでしまいます。
// ❌ 間違い(forEachは非同期処理を待たない)
const urls = ['url1', 'url2', 'url3'];
urls.forEach(async (url) => {
const data = await fetch(url); // 待たずに次のループへ
});
// ✅ 正解(for...ofを使う)
for (const url of urls) {
const data = await fetch(url); // ちゃんと待つ
}
// ✅ または map + Promise.all
const promises = urls.map(url => fetch(url));
const results = await Promise.all(promises);
3. トップレベルでawaitを使う
JavaScriptのファイルのトップレベル(関数の外)ではawaitが使えません。
ただし、Node.js 14.8以降や最新のブラウザでは、ESModule形式でトップレベルawaitがサポートされています。
// ❌ 間違い(トップレベルでは使えない)
const data = await fetch('/api/data');
// ✅ 正解1(async関数でラップ)
async function main() {
const data = await fetch('/api/data');
}
main();
// ✅ 正解2(即時実行関数)
(async () => {
const data = await fetch('/api/data');
})();
実用的なパターン集
リトライ処理
ネットワークエラーは一時的なものであることが多く、再試行することで成功する場合があります。
以下は、指定回数まで自動リトライする実装例です。
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (response.ok) {
return await response.json();
}
} catch (error) {
console.log(`試行 ${i + 1} 失敗:`, error.message);
// 最後の試行でなければ待機
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
throw new Error('最大リトライ回数を超えました');
}
タイムアウト処理
APIコールが長時間応答しない場合、タイムアウトを設定して処理を中断することが重要です。
AbortControllerを使うことで、fetchリクエストをキャンセルできます。
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);
try {
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('タイムアウトしました');
}
throw error;
}
}
キャッシュ処理
同じデータを何度も取得するのは非効率的です。
一度取得したデータをキャッシュすることで、APIコールの数を減らし、
アプリケーションのパフォーマンスを向上させることができます。
const cache = new Map();
async function fetchWithCache(url) {
// キャッシュがあれば返す
if (cache.has(url)) {
console.log('キャッシュから取得');
return cache.get(url);
}
// なければフェッチ
console.log('APIから取得');
const response = await fetch(url);
const data = await response.json();
// キャッシュに保存
cache.set(url, data);
return data;
}
デバッグのコツ
非同期処理のデバッグは難しいと思われがちですが、いくつかのテクニックを知っていれば、
効率的に問題を特定できます。
1. console.logで確認
async function debugExample() {
console.log('処理開始');
const data = await fetchData();
console.log('データ取得完了:', data);
const processed = await processData(data);
console.log('処理完了:', processed);
return processed;
}
2. Chrome DevToolsの活用
ブラウザの開発者ツールは非同期処理のデバッグに非常に役立ちます:
-
Networkタブ:
- APIのリクエストとレスポンスを確認
- リクエストのタイミングや所要時間を分析
- エラーステータスを確認(404、500など)
-
Consoleタブ:
- エラーメッセージとスタックトレースを確認
- console.logの出力を確認
- Promiseの状態を確認
-
Sourcesタブ:
- async関数にブレークポイントを設定
- awaitの前後でステップ実行
- 変数の値をリアルタイムで確認
3. エラースタックトレース
async function betterError() {
try {
await someAsyncFunction();
} catch (error) {
console.error('エラー発生箇所:', error.stack);
throw error; // 再スロー
}
}
まとめ
async/awaitは「非同期処理を同期処理のように書ける」魔法の構文です。
この記事で学んだことを振り返ると:
覚えておくポイント:
- async: 非同期関数の宣言
- await: Promiseの完了を待つ
- try/catch: エラー処理
- Promise.all: 並列実行
使いどころ:
- API通信
- ファイル読み書き
- データベース操作
- 時間のかかる処理全般
あなたのコードはこう変わります:
- 可読性の向上: ネストが深いコールバックから、直線的で読みやすいコードへ
- メンテナンス性の向上: 新しい開発者が参加しても、すぐにコードを理解できます
- デバッグの容易さ: ステップ実行で簡単に処理の流れを追えます
- エラーハンドリングの簡素化: try/catchで統一的にエラー処理ができます
最初は「Promiseで十分」と思うかもしれませんが、async/awaitを使い始めると、もう戻れなくなります。
最後に覚えておくべきこと:
非同期処理は最初は難しく感じるかもしれませんが、以下のポイントさえ押さえておけば大丈夫です:
- 「時間がかかる処理にはawait」 を付ける
- awaitを使う関数にはasyncを付ける
- エラー処理はtry/catchで囲む
- 並列処理にはPromise.allを使う
この4つをマスターすれば、非同期処理は完璧に理解していると言えるでしょう!