前回の記事 では、DOM操作とイベント処理について紹介しました。今回は、非同期処理について、Promise、async/await構文、そしてAPI通信の例を中心に紹介します。
非同期処理とは
非同期処理とは、時間のかかる処理をバックグラウンドで実行しながら、他の処理を続行する仕組みです。WebアプリケーションでAPI通信やファイル読み込みなどを行なう際に有用です。
同期処理と非同期処理の違い
// 同期処理の例(ブロッキング)
console.log('1番目の処理');
console.log('2番目の処理'); // 1番目が完了してから実行
console.log('3番目の処理'); // 2番目が完了してから実行
// 非同期処理の例(ノンブロッキング)
console.log('1番目の処理');
setTimeout(() => {
console.log('2番目の処理(遅延)');
}, 1000);
console.log('3番目の処理'); // 2番目を待たずに実行
// 実行結果:
// 1番目の処理
// 3番目の処理
// 2番目の処理(遅延)← 1秒後に表示
コールバック地獄(Callback Hell)
従来のコールバック関数による非同期処理は、処理が複雑になると読みにくいコードになってしまいます。
// コールバック地獄の例
function getUser(userId, callback) {
setTimeout(() => {
console.log('ユーザー情報を取得中...');
callback({ id: userId, name: 'Alice' });
}, 1000);
}
function getUserPosts(userId, callback) {
setTimeout(() => {
console.log('投稿一覧を取得中...');
callback(['投稿1', '投稿2', '投稿3']);
}, 1000);
}
function getPostComments(postId, callback) {
setTimeout(() => {
console.log('コメント一覧を取得中...');
callback(['コメント1', 'コメント2']);
}, 1000);
}
// 複数の非同期処理を連続で実行(コールバック地獄)
getUser(1, (user) => {
console.log('ユーザー:', user);
getUserPosts(user.id, (posts) => {
console.log('投稿:', posts);
getPostComments(posts[0], (comments) => {
console.log('コメント:', comments);
// さらに深いネストが続く可能性...
});
});
});
Promise(プロミス)
Promiseは、非同期処理をより読みやすく、管理しやすくするために導入された仕組みです。
Promiseの基本概念
// Promiseの基本構造
const promise = new Promise((resolve, reject) => {
// 非同期処理
// ここでは何らかの処理を行なったものとして、
// 成功/失敗の結果を変数に格納している
const success = true;
if (success) {
resolve('成功時の値'); // 成功時にresolveを呼ぶ
} else {
reject('失敗時のエラー'); // 失敗時にrejectを呼ぶ
}
});
// Promiseの利用
promise
.then((result) => {
console.log('成功:', result); // 成功(resolveが呼ばれた)時に実行される
})
.catch((error) => {
console.log('エラー:', error); // 失敗(rejectが呼ばれた)時に実行される
})
.finally(() => {
console.log('完了(成功・失敗問わず実行)');
});
Promiseの3つの状態
// 1. Pending(待機中)- 初期状態
const pendingPromise = new Promise((resolve, reject) => {
// まだ resolve も reject も呼ばれていない状態
});
// 2. Fulfilled(履行済み)- 成功した状態
const fulfilledPromise = Promise.resolve('成功');
// 3. Rejected(拒否済み)- 失敗した状態
const rejectedPromise = Promise.reject('失敗');
console.log(pendingPromise); // Promise { <pending> }
console.log(fulfilledPromise); // Promise { '成功' }
console.log(rejectedPromise); // Promise { <rejected> '失敗' }
rejectedPromise.catch(() => {}); // Uncaught (in promise) エラーの場合に catch を使うことでエラーを処理する
Promiseチェーン
先ほどのコールバック地獄のコードをPromiseで書き換えると、以下のようになります。
function getUserPromise(userId) {
return new Promise((resolve) => {
setTimeout(() => {
console.log('ユーザー情報を取得中...');
resolve({ id: userId, name: 'Alice' });
}, 1000);
});
}
function getUserPostsPromise(userId) {
return new Promise((resolve) => {
setTimeout(() => {
console.log('投稿一覧を取得中...');
resolve(['投稿1', '投稿2', '投稿3']);
}, 1000);
});
}
function getPostCommentsPromise(postId) {
return new Promise((resolve) => {
setTimeout(() => {
console.log('コメント一覧を取得中...');
resolve(['コメント1', 'コメント2']);
}, 1000);
});
}
// Promiseチェーンで順次実行
getUserPromise(1)
.then((user) => {
console.log('ユーザー:', user);
return getUserPostsPromise(user.id);
})
.then((posts) => {
console.log('投稿:', posts);
return getPostCommentsPromise(posts[0]);
})
.then((comments) => {
console.log('コメント:', comments);
})
.catch((error) => {
console.error('エラーが発生しました:', error);
});
Promise.allとPromise.race
先ほどまでのコードは順次実行でしたが、複数のPromiseを並行しての実行もできます。
// 複数のPromiseを並行実行
const promise1 = Promise.resolve('結果1');
const promise2 = Promise.resolve('結果2');
const promise3 = Promise.resolve('結果3');
// Promise.all - すべてのPromiseが完了するまで待つ
Promise.all([promise1, promise2, promise3])
.then((results) => {
console.log('すべて完了:', results); // ['結果1', '結果2', '結果3']
})
.catch((error) => {
console.log('いずれかが失敗:', error);
});
// Promise.race - 最初に完了したPromiseの結果を返す
Promise.race([promise1, promise2, promise3])
.then((result) => {
console.log('最初に完了:', result); // 最初に完了したPromiseの結果
});
// 実用例:タイムアウト付きのAPIリクエスト
function fetchWithTimeout(url, timeout = 5000) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('タイムアウト')), timeout);
});
return Promise.race([fetchPromise, timeoutPromise]);
}
async/await構文
async/await構文は、Promiseをより同期処理のように書けるようにするためのものです。
async/awaitの基本
async をつけた関数は常にPromiseを返すため、手動で resolve() や reject() を呼ぶ必要がありません。
また、await キーワードを使うことで、Promiseの完了を待つことができます。たとえば、以下のコードの場合、getUserPostsPromise() は getUserPromise() が完了するまで実行されません。
// Promiseチェーンで書いた処理をasync/awaitで書き直し
async function processUserData() {
try {
// awaitで非同期処理を待つ
const user = await getUserPromise(1);
console.log('ユーザー:', user);
const posts = await getUserPostsPromise(user.id);
console.log('投稿:', posts);
const comments = await getPostCommentsPromise(posts[0]);
console.log('コメント:', comments);
return comments; // async関数は常にPromiseを返す
} catch (error) {
console.error('エラーが発生しました:', error);
}
}
// async関数の呼び出し
processUserData()
.then((result) => {
console.log('最終結果:', result);
});
async関数の特徴
// async関数は常にPromiseを返す
async function getValue() {
return 'Hello World'; // 自動的にPromise.resolve('Hello World')になる
}
getValue().then(console.log); // "Hello World"
// エラーを投げるとrejectedなPromiseを返す
async function getError() {
throw new Error('エラーが発生しました'); // 自動的にPromise.reject(new Error('エラーが発生しました'))になる
}
getError().catch(console.error); // Error: エラーが発生しました
// awaitは基本的にasync関数内でのみ使用可能
async function example() {
const result = await Promise.resolve('成功');
console.log(result); // "成功"
}
fetch APIを使った例
fetch APIは、HTTP通信を行なうためのAPIです。async/awaitと組み合わせることで、わかりやすいコードが書けます。
基本的なfetch APIの使用
以下のコードでは、サンプルデータを提供する JSON Placeholder から架空のユーザー情報を取得しています。
// GET リクエスト
async function fetchUsers() {
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);
return users;
} catch (error) {
console.error('ユーザー取得エラー:', error);
throw error; // 呼び出し元にエラーを伝播
}
}
// POST リクエスト
async function createUser(userData) {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
const newUser = await response.json();
console.log('新しいユーザー:', newUser);
return newUser;
} catch (error) {
console.error('ユーザー作成エラー:', error);
throw error;
}
}
// 使用例
fetchUsers();
createUser({ name: 'John Doe', email: 'john@example.com' });
実用的なAPI通信クラス
以下は、CRUD操作を行なうためのAPI通信クラスの例です。
// API通信を管理するクラス
class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
};
try {
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// レスポンスの Content-Type を確認
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
} else {
return await response.text();
}
} catch (error) {
console.error(`API Request Error (${endpoint}):`, error);
throw error;
}
}
async get(endpoint) {
return this.request(endpoint);
}
async post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data),
});
}
async put(endpoint, data) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async delete(endpoint) {
return this.request(endpoint, {
method: 'DELETE',
});
}
}
// 使用例
const api = new ApiClient('https://jsonplaceholder.typicode.com');
async function userOperations() {
try {
// ユーザー一覧を取得
const users = await api.get('/users');
console.log('ユーザー一覧:', users.slice(0, 3)); // 最初の3件
// 特定のユーザーを取得
const user = await api.get('/users/1');
console.log('ユーザー詳細:', user);
// 新しいユーザーを作成
const newUser = await api.post('/users', {
name: 'Test User',
email: 'test@example.com',
});
console.log('作成されたユーザー:', newUser);
} catch (error) {
console.error('操作中にエラーが発生しました:', error);
}
}
userOperations();
エラーハンドリングの例
// エラーハンドリングの例
class UserService {
constructor() {
this.api = new ApiClient('https://jsonplaceholder.typicode.com');
}
async getUser(userId) {
try {
const user = await this.api.get(`/users/${userId}`);
return { success: true, data: user };
} catch (error) {
return {
success: false,
error: error.message,
type: this.getErrorType(error)
};
}
}
async getUserWithRetry(userId, maxRetries = 3) {
let attempt = 1;
while (attempt <= maxRetries) {
try {
const user = await this.api.get(`/users/${userId}`);
return { success: true, data: user };
} catch (error) {
console.warn(`取得失敗 (試行 ${attempt}/${maxRetries}):`, error.message);
if (attempt === maxRetries) {
return {
success: false,
error: `${maxRetries}回試行しましたが失敗しました: ${error.message}`,
type: 'retry_exhausted'
};
}
// 指数バックオフで待機
await this.delay(Math.pow(2, attempt) * 1000);
attempt++;
}
}
}
getErrorType(error) {
if (error.message.includes('404')) return 'not_found';
if (error.message.includes('500')) return 'server_error';
if (error.message.includes('Failed to fetch')) return 'network_error';
return 'unknown_error';
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// 使用例
const userService = new UserService();
async function demonstrateErrorHandling() {
// 正常なケース
const result1 = await userService.getUser(1);
if (result1.success) {
console.log('ユーザー取得成功:', result1.data.name);
} else {
console.error('エラー:', result1.error);
}
// 存在しないユーザー(404エラー)
const result2 = await userService.getUser(999);
if (!result2.success) {
console.error('エラータイプ:', result2.type); // "not_found"
}
// リトライ機能付きでの取得
const result3 = await userService.getUserWithRetry(1);
console.log('リトライ結果:', result3.success ? '成功' : '失敗');
}
demonstrateErrorHandling();
発展:イベントループの仕組み
JavaScriptの非同期処理を理解するうえで、イベントループの仕組みを簡単に紹介します。
// イベントループの動作例
console.log('1: 同期処理');
setTimeout(() => {
console.log('2: マクロタスク(Timer)');
}, 0);
Promise.resolve().then(() => {
console.log('3: マイクロタスク(Promise)');
});
console.log('4: 同期処理');
// 実行順序:
// 1: 同期処理
// 4: 同期処理
// 3: マイクロタスク(Promise)
// 2: マクロタスク(Timer)
// 理由:
// 1. 同期処理が最優先で実行される
// 2. マイクロタスク(Promise)がマクロタスク(Timer)より優先される
復習
問題
1. 以下のコードの実行順序を予想する
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
2. fetch APIを使って、指定されたURLからデータを取得し、エラーハンドリングも含めたasync関数を作成する
たとえば、このasync関数を fetchData(url) とし、https://jsonplaceholder.typicode.com/posts/1 からデータを取得する場合、以下のように使用します。
fetchData('https://jsonplaceholder.typicode.com/posts/1')
.then(post => console.log('投稿タイトル:', post.title))
.catch(error => console.error('処理に失敗しました:', error.message));
3. 次のコールバック関数を使った処理を、async/awaitを使って書き換える
function fetchUserData(userId, callback) {
setTimeout(() => {
if (userId > 0) {
callback(null, { id: userId, name: 'User ' + userId });
} else {
callback(new Error('Invalid user ID'), null);
}
}, 1000);
}
// 使用例
fetchUserData(1, (error, user) => {
if (error) {
console.error('エラー:', error.message);
} else {
console.log('ユーザー:', user);
}
});
解答例
1. 実行順序
// 実行順序: A → D → C → B
// 理由:
// 1. 同期処理(A, D)が最初に実行される
// 2. マイクロタスク(Promise.then)がマクロタスク(setTimeout)より優先される
2. コールバックからasync/awaitへの書き換え
// Promise版の関数を作成
function fetchUserDataPromise(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId > 0) {
resolve({ id: userId, name: 'User ' + userId });
} else {
reject(new Error('Invalid user ID'));
}
}, 1000);
});
}
// async/awaitを使った書き換え
async function getUserData(userId) {
try {
console.log(`ユーザー${userId}のデータを取得中...`);
const user = await fetchUserDataPromise(userId);
console.log('ユーザー:', user);
return user;
} catch (error) {
// エラーは呼び出し元で処理するため、ここではスローするだけにする
throw error;
}
}
// 使用例
async function example() {
await getUserData(1); // 正常なケース
await getUserData(-1); // エラーケース
}
example().catch(console.error);
3. fetch APIを使ったデータ取得関数
async function fetchData(url) {
try {
console.log(`データ取得開始: ${url}`);
const response = await fetch(url);
// HTTPステータスの確認
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
}
// Content-Typeに応じてパース方法を変更
const contentType = response.headers.get('content-type');
let data;
if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}
console.log('データ取得成功:', data);
return data;
} catch (error) {
console.error('データ取得エラー:', error.message);
// エラーの種類に応じた処理
if (error.name === 'TypeError') {
console.error('ネットワークエラーまたはCORSエラーの可能性があります');
}
throw error; // 呼び出し元にエラーを伝播
}
}
// 使用例
fetchData('https://jsonplaceholder.typicode.com/posts/1')
.then(post => console.log('投稿タイトル:', post.title))
.catch(error => console.error('処理に失敗しました:', error.message));
まとめ
非同期処理について、Promise、async/await構文、そして実践的なAPI通信の方法を紹介しました。
技術的メリット
-
コードの可読性向上
- コールバック地獄の解消
- エラーハンドリングの明確化
- async/awaitにより同期処理のような書き方が可能
-
パフォーマンスの向上
- 並行処理による実行時間の短縮
- ノンブロッキング処理によるユーザー体験の向上
-
Promise.allによる複数処理
-
エラー処理の改善
- try/catch文による統一的なエラーハンドリング
-
Promise.catchによるエラー処理の一元化 - リトライ機能の実装が容易
-
実用性の高い機能
- fetch APIによるHTTP通信
- RESTful APIとの連携
- 実際のWebアプリケーション開発で即活用可能
活用場面の例
-
API通信
REST API、GraphQL、WebSocket通信 -
ファイル操作
画像・動画のアップロード、ダウンロード -
データベースアクセス
クエリ実行、トランザクション処理 -
ユーザーインターフェイス
ローディング状態の管理、プログレス表示
注意すべきポイント
非同期処理は便利な反面、以下の点に注意が必要です。
- エラーハンドリング:非同期処理にはエラー処理を実装
- メモリリークの防止:不要なPromiseや非同期処理のクリーンアップ
- パフォーマンスの考慮:過度な並行処理によるリソース消費への注意
- デバッグの困難さ:非同期処理特有のデバッグ手法の習得
次回 は、最新のJavaScript機能と開発の今後の展望について紹介します。