目次
非同期通信とは?基本から理解しよう
「非同期通信って何となく使っているけど、正確に説明できるか自信がない…」そんな悩みを抱えている開発者の方は少なくありません。
モダンなJavaScript開発において、非同期処理は避けて通れない重要な概念です。API呼び出し、データベースアクセス、ファイル操作など、実務で扱う多くの処理が非同期で動作します。この記事では、非同期通信の基本から実践的な活用方法まで、段階的に解説します。
非同期通信と同期通信の違い
非同期通信とは、送信者が受信者の応答を待たずに次の処理を実行できる通信方式です。一方、同期通信では処理が完了するまで次の処理に進めません。
同期処理の例:
console.log('処理1: 開始');
const result = heavyCalculation(); // 3秒かかる
console.log('処理2: 計算結果は' + result);
console.log('処理3: 完了');
// 出力: 処理1 → (3秒待機) → 処理2 → 処理3
非同期処理の例:
console.log('処理1: 開始');
setTimeout(() => {
console.log('処理2: 3秒後の処理');
}, 3000);
console.log('処理3: すぐに実行');
// 出力: 処理1 → 処理3 → (3秒待機) → 処理2
この違いが、ユーザー体験とアプリケーションのパフォーマンスに大きな影響を与えます。
JavaScriptで非同期処理が必要な理由
JavaScriptはシングルスレッドで動作するため、一度に一つの処理しか実行できません。同期処理だけで開発すると、以下の問題が発生します。
UIのフリーズ
// 同期的なAPI呼び出し(悪い例)
const data = fetchDataSync('https://api.example.com/users'); // 5秒かかる
// この間、画面が完全にフリーズし、ユーザーは何も操作できない
非同期処理を使うことで、以下のメリットが得られます。
- レスポンシブなUI:待ち時間中も他の操作が可能
- 効率的なリソース活用:待ち時間中に別の処理を実行
- 複数処理の並行実行:複数のAPI呼び出しを同時に処理
実務でよくある非同期処理の例
API呼び出し
fetch('https://api.example.com/user/123')
.then(response => response.json())
.then(user => console.log(user.name));
ファイル読み込み(Node.js)
const fs = require('fs').promises;
fs.readFile('large-file.txt', 'utf8')
.then(data => console.log('読み込み完了'));
タイマー処理
setTimeout(() => {
showNotification('処理が完了しました');
}, 3000);
これらの処理は完了までに時間がかかる可能性があるため、非同期で実装する必要があります。
コールバックからPromiseへの進化
コールバック関数による非同期処理
JavaScriptで最も古くから使われてきた非同期処理の方法がコールバック関数です。処理が完了した後に実行される関数を引数として渡す仕組みです。
function fetchUser(userId, callback) {
setTimeout(() => {
const user = { id: userId, name: '山田太郎' };
callback(user);
}, 1000);
}
fetchUser(123, (user) => {
console.log(`ユーザー名: ${user.name}`);
});
コールバック地獄の問題
複数の非同期処理を連続して実行する場合、「コールバック地獄」と呼ばれる問題が発生します。
getUser(userId, (error, user) => {
if (error) return console.error(error);
getPosts(user.id, (error, posts) => {
if (error) return console.error(error);
getComments(posts[0].id, (error, comments) => {
if (error) return console.error(error);
getCommentDetails(comments[0].id, (error, details) => {
if (error) return console.error(error);
console.log(details);
});
});
});
});
このピラミッド構造には以下の問題があります。
- 可読性の低下:コードが横に長くなり読みづらい
- 保守性の悪化:修正や機能追加が困難
- エラー処理の重複:各階層でエラーハンドリングが必要
Promiseの登場
コールバック地獄を解決するため、ES2015でPromiseが標準仕様として導入されました。
Promiseは非同期処理の最終的な完了または失敗を表すオブジェクトで、以下の3つの状態を持ちます。
- Pending(待機中):初期状態
- Fulfilled(成功):処理が成功して完了
- Rejected(失敗):処理が失敗
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve('処理成功');
} else {
reject('処理失敗');
}
}, 1000);
});
promise
.then(result => console.log(result))
.catch(error => console.error(error));
Promiseの大きな利点はチェーン(連鎖)できることです。
getUser(userId)
.then(user => getPosts(user.id))
.then(posts => getComments(posts[0].id))
.then(comments => getCommentDetails(comments[0].id))
.then(details => console.log(details))
.catch(error => console.error('エラー:', error));
ネストが解消され、フラットで読みやすいコードになりました。
Promiseの主要メソッド
then() – 成功時の処理
fetch('https://api.example.com/user')
.then(response => response.json())
.then(data => {
console.log('ユーザー名:', data.name);
return data.id; // 次のthenに値を渡せる
})
.then(userId => console.log('ユーザーID:', userId));
catch() – エラー処理
fetch('https://api.example.com/user')
.then(response => {
if (!response.ok) throw new Error('HTTPエラー');
return response.json();
})
.catch(error => console.error('エラーが発生:', error.message));
finally() – 成功・失敗に関わらず実行
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error))
.finally(() => {
isLoading = false;
console.log('処理完了');
});
async/awaitでより直感的な記述を
ES2017で導入されたasync/awaitにより、非同期処理をさらに直感的に書けるようになりました。
async/awaitの基本
async/awaitは、Promiseをより同期処理のように書けるようにする構文です。
async function getUser() {
const response = await fetch('https://api.example.com/user');
const user = await response.json();
return user;
}
getUser().then(user => console.log(user.name));
重要なポイント:
- async関数は必ずPromiseを返す
- awaitはasync関数の中でのみ使用可能
- awaitを付けた処理が完了するまで次の行は実行されない
Promiseとの比較
// Promiseチェーン
function getUserPosts() {
return fetch('https://api.example.com/user/123')
.then(response => response.json())
.then(user => fetch(`https://api.example.com/posts?userId=${user.id}`))
.then(response => response.json());
}
// async/await
async function getUserPosts() {
const userResponse = await fetch('https://api.example.com/user/123');
const user = await userResponse.json();
const postsResponse = await fetch(`https://api.example.com/posts?userId=${user.id}`);
const posts = await postsResponse.json();
return posts;
}
async/await版の方が処理の流れが一目で理解できます。
エラーハンドリング(try-catch)
async/awaitでは、try-catch文を使って同期処理と同じようにエラーを処理できます。
async function fetchUser(userId) {
try {
const response = await fetch(`https://api.example.com/user/${userId}`);
if (!response.ok) {
throw new Error(`HTTPエラー: ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error('ユーザー取得エラー:', error.message);
throw error;
}
}
複数の処理でのエラーハンドリング
async function processOrder(orderId) {
try {
const order = await getOrder(orderId);
const stock = await checkStock(order.productId);
if (stock < order.quantity) {
throw new Error('在庫不足');
}
const payment = await processPayment(order);
const shipping = await arrangeShipping(order);
return { success: true, trackingNumber: shipping.trackingNumber };
} catch (error) {
console.error('注文処理エラー:', error.message);
if (error.message === '在庫不足') {
await notifyOutOfStock(orderId);
}
return { success: false, error: error.message };
} finally {
hideLoadingSpinner();
}
}
複数の非同期処理を効率的に扱う
Promise.allで並列処理
Promise.allは、複数のPromiseを並列実行し、すべてが完了するまで待つメソッドです。
async function fetchAllData() {
const [users, posts, comments] = await Promise.all([
fetch('https://api.example.com/users').then(r => r.json()),
fetch('https://api.example.com/posts').then(r => r.json()),
fetch('https://api.example.com/comments').then(r => r.json())
]);
return { users, posts, comments };
}
実行時間の比較
// 順次実行(約3秒)
async function fetchSequential() {
const users = await fetch('/api/users').then(r => r.json()); // 1秒
const posts = await fetch('/api/posts').then(r => r.json()); // 1秒
const comments = await fetch('/api/comments').then(r => r.json()); // 1秒
return { users, posts, comments };
}
// 並列実行(約1秒)
async function fetchParallel() {
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return { users, posts, comments };
}
重要な特性:
- すべてのPromiseが成功する必要がある
- 1つでも失敗すると、Promise.all全体が失敗する
- 並列実行されるため、処理時間が大幅に短縮される
Promise.allSettledで部分的な失敗に対応
Promise.allの問題点を解決するのがPromise.allSettledです。
async function fetchMultipleUsers(ids) {
const results = await Promise.allSettled(
ids.map(id => fetch(`/api/users/${id}`).then(r => r.json()))
);
// 成功したものだけを抽出
const users = results
.filter(result => result.status === 'fulfilled')
.map(result => result.value);
// 失敗したものをログ出力
const errors = results
.filter(result => result.status === 'rejected')
.map(result => result.reason);
if (errors.length > 0) {
console.warn(`${errors.length}件の取得に失敗:`, errors);
}
return users;
}
Promise.raceで最速の結果を取得
Promise.raceは、複数のPromiseのうち最初に完了したものの結果を返すメソッドです。
タイムアウト処理の実装
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('タイムアウト')), ms);
});
}
async function fetchWithTimeout(url, timeoutMs = 5000) {
try {
const response = await Promise.race([
fetch(url),
timeout(timeoutMs)
]);
return await response.json();
} catch (error) {
if (error.message === 'タイムアウト') {
console.error('APIの応答が遅すぎます');
}
throw error;
}
}
使い分けのポイント
| メソッド | 用途 | 特徴 |
|---|---|---|
| Promise.all | 全て成功が必要 | 1つでも失敗すると全体が失敗 |
| Promise.allSettled | 部分的な失敗を許容 | 全ての結果を返す |
| Promise.race | 最速の結果が必要 | 最初に完了したものを返す |
| Promise.any | いずれか1つ成功すればOK | 最初の成功を返す |
よくあるつまずきポイントと対処法
awaitの付け忘れ
最も多いミスの一つが、awaitを付け忘れることです。
// 間違い
async function getUserData() {
const response = fetch('/api/user'); // awaitを忘れている
const data = response.json(); // エラー
return data;
}
// 正しい
async function getUserData() {
const response = await fetch('/api/user');
const data = await response.json();
return data;
}
配列処理での注意点
// forEachは非同期を待たない(間違い)
async function processUsers(userIds) {
const results = [];
userIds.forEach(async (id) => {
const user = await fetchUser(id);
results.push(user);
});
return results; // 空の配列が返る
}
// for...ofを使う(正しい)
async function processUsers(userIds) {
const results = [];
for (const id of userIds) {
const user = await fetchUser(id);
results.push(user);
}
return results;
}
// Promise.allで並列処理(より効率的)
async function processUsers(userIds) {
const promises = userIds.map(id => fetchUser(id));
return await Promise.all(promises);
}
並列処理と直列処理の混同
依存関係がない処理は並列実行すべきです。
// 非効率(直列処理:3秒)
async function fetchAllData() {
const users = await fetch('/api/users').then(r => r.json());
const posts = await fetch('/api/posts').then(r => r.json());
const comments = await fetch('/api/comments').then(r => r.json());
return { users, posts, comments };
}
// 効率的(並列処理:1秒)
async function fetchAllData() {
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return { users, posts, comments };
}
判断基準:
- 依存関係がない → 並列処理(Promise.all)
- 前の結果が必要 → 直列処理(await)
デバッグのコツ
ログ出力のベストプラクティス
async function fetchWithLogging(url) {
console.log(`[開始] ${url}`);
const startTime = Date.now();
try {
const response = await fetch(url);
const data = await response.json();
const duration = Date.now() - startTime;
console.log(`[成功] ${url} (${duration}ms)`);
return data;
} catch (error) {
const duration = Date.now() - startTime;
console.error(`[失敗] ${url} (${duration}ms)`, error);
throw error;
}
}
デバッグ時のチェックリスト:
- ✅ awaitを付け忘れていないか
- ✅ エラーハンドリングは適切か
- ✅ 並列処理と直列処理を混同していないか
- ✅ タイムアウト処理は設定されているか
まとめ:非同期処理を理解して実務で活かす
学んだことの整理
基本概念
- 非同期通信は処理の完了を待たずに次の処理を実行できる仕組み
- JavaScriptのシングルスレッドモデルでも効率的な処理が可能
- コールバック地獄を避けるために、Promiseやasync/awaitが登場
Promiseとasync/await
- Promiseは非同期処理の結果を表すオブジェクト
- async/awaitで非同期処理を同期的に記述
- try-catchで直感的なエラーハンドリング
複数処理の制御
- Promise.allで並列処理、全て成功が必要
- Promise.allSettledで部分的な失敗を許容
- Promise.raceで最速の結果を取得
実務で活かすポイント
1. エラーハンドリングを徹底する
async function robustFetch(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
logError(error);
showUserMessage('データの取得に失敗しました');
return null;
}
}
2. パフォーマンスを意識する
- 不要な直列処理を避ける
- 大量データはページネーションで処理
- キャッシュを活用する
3. テストを書く
test('ユーザーデータを取得できる', async () => {
const user = await fetchUser(1);
expect(user).toHaveProperty('id');
expect(user).toHaveProperty('name');
});
次のステップ
実践的な学習方法
- 公開APIを使って練習(JSONPlaceholder、GitHub APIなど)
- 小さなプロジェクトを作る(ユーザー検索アプリ、Todoリストなど)
- MDN Web Docsやオンラインコミュニティで学習を深める
学習はインプットとアウトプットの繰り返しが大切です。読んで理解したら実際にコードを書き、動作を確認しながら理解を深めていきましょう。
技術的な課題でお困りの際は
Harmonic Societyでは、中小企業の技術課題をサポートしています。「テクノロジーと人間性の調和」を理念に、企業ごとに”ちょうどいい”デジタル化を支援しています。
- 技術相談・コンサルティング
- 既存システムのリファクタリング支援
- チーム内での技術勉強会の実施
- AI活用による開発効率化のサポート
非同期処理の実装やモダンなJavaScript開発、Web APIの設計など、実務で直面する課題について具体的なアドバイスを提供できます。お気軽にご相談ください。
