はじめに
Promiseとは何か
Promiseは、JavaScriptで非同期処理を扱うための仕組みです。「約束」という名前の通り、「将来的に値が返ってくることを約束するオブジェクト」として機能します。
非同期処理とは、処理の完了を待たずに次の処理を実行できる仕組みのことです。例えば、サーバーからデータを取得する際、データが返ってくるまで待つ必要がありますが、その間に他の処理を進めることができます。
なぜPromiseが必要なのか
従来のコールバック関数だけでは、複数の非同期処理を連続して実行する際にコードが複雑になってしまいます。Promiseを使うことで、非同期処理をより直感的で読みやすいコードで書けるようになります。
非同期処理の基礎知識
同期処理と非同期処理の違い
同期処理は、1つの処理が完了してから次の処理に進む方式です。一方、非同期処理は処理の完了を待たずに次の処理を進められます。
// 同期処理の例
console.log('1つ目');
console.log('2つ目');
console.log('3つ目');
// 出力: 1つ目、2つ目、3つ目の順
// 非同期処理の例
console.log('1つ目');
setTimeout(() => {
console.log('2つ目');
}, 1000);
console.log('3つ目');
// 出力: 1つ目、3つ目、2つ目の順
コールバック関数の限界
コールバック関数を使った非同期処理では、複数の処理を連続して実行すると、ネストが深くなり読みにくくなります。これを「コールバック地獄」と呼びます。
// コールバック地獄の例
getData1(function(result1) {
getData2(result1, function(result2) {
getData3(result2, function(result3) {
getData4(result3, function(result4) {
// さらにネストが続く...
});
});
});
});
Promiseの基本
Promiseの構文
Promiseは以下のように作成します。
const promise = new Promise((resolve, reject) => {
// 非同期処理
const success = true;
if (success) {
resolve('成功しました');
} else {
reject('失敗しました');
}
});
Promiseのコンストラクタには、2つの引数を持つ関数を渡します。
- resolve: 処理が成功した際に呼び出す関数
- reject: 処理が失敗した際に呼び出す関数
Promiseの3つの状態
Promiseは常に以下の3つの状態のいずれかを持ちます。
- Pending(待機中): 初期状態。処理がまだ完了していない
- Fulfilled(成功): 処理が正常に完了した
- Rejected(拒否): 処理が失敗した
resolveとrejectの役割
resolveは処理が成功した際に結果を返すために使います。rejectは処理が失敗した際にエラー情報を返すために使います。
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
if (userId) {
// ユーザーデータ取得に成功
resolve({ id: userId, name: 'Taro' });
} else {
// ユーザーIDが無効
reject('ユーザーIDが指定されていません');
}
});
}
Promiseの使い方
.then()メソッドで成功時の処理
Promiseが成功した際の処理は.then()メソッドで記述します。
fetchUserData(1)
.then((userData) => {
console.log('ユーザー名:', userData.name);
});
.then()メソッドは、resolveに渡された値を引数として受け取ります。
.catch()メソッドでエラーハンドリング
Promiseが失敗した際の処理は.catch()メソッドで記述します。
fetchUserData(null)
.catch((error) => {
console.error('エラー:', error);
});
.finally()メソッドで共通処理
成功・失敗に関わらず実行したい処理は.finally()メソッドで記述します。
fetchUserData(1)
.then((userData) => {
console.log('成功:', userData);
})
.catch((error) => {
console.error('失敗:', error);
})
.finally(() => {
console.log('処理が完了しました');
});
Promiseのチェーン
メソッドチェーンの書き方
.then()メソッドは新しいPromiseを返すため、連続して記述できます。
fetchUserData(1)
.then((userData) => {
console.log('ユーザー取得:', userData.name);
return userData.id;
})
.then((userId) => {
console.log('ユーザーID:', userId);
return userId * 2;
})
.then((result) => {
console.log('計算結果:', result);
})
.catch((error) => {
console.error('エラー:', error);
});
複数の非同期処理を順番に実行する
Promiseチェーンを使えば、複数の非同期処理を順番に実行できます。
function getUser(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: userId, name: 'Taro' });
}, 1000);
});
}
function getPosts(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([{ id: 1, title: '記事1' }, { id: 2, title: '記事2' }]);
}, 1000);
});
}
getUser(1)
.then((user) => {
console.log('ユーザー:', user.name);
return getPosts(user.id);
})
.then((posts) => {
console.log('投稿数:', posts.length);
})
.catch((error) => {
console.error('エラー:', error);
});
Promiseの便利なメソッド
Promise.all() - 複数のPromiseを並列実行
Promise.all()は、複数のPromiseを並列で実行し、すべてが完了するまで待ちます。すべてのPromiseが成功した場合のみ、結果の配列を返します。
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve) => setTimeout(() => resolve('foo'), 100));
const promise3 = Promise.resolve(42);
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log(values); // [3, 'foo', 42]
})
.catch((error) => {
console.error('いずれかのPromiseが失敗:', error);
});
1つでもPromiseが失敗すると、Promise.all()全体が失敗します。
Promise.race() - 最初に完了したPromiseを取得
Promise.race()は、最初に完了したPromiseの結果を返します。
const promise1 = new Promise((resolve) => setTimeout(() => resolve('1番'), 500));
const promise2 = new Promise((resolve) => setTimeout(() => resolve('2番'), 100));
Promise.race([promise1, promise2])
.then((value) => {
console.log(value); // '2番'
});
タイムアウト処理の実装などに使えます。
Promise.allSettled() - すべての結果を取得
Promise.allSettled()は、すべてのPromiseが完了するまで待ち、成功・失敗に関わらずすべての結果を返します。
const promise1 = Promise.resolve(3);
const promise2 = Promise.reject('エラー');
const promise3 = Promise.resolve(42);
Promise.allSettled([promise1, promise2, promise3])
.then((results) => {
results.forEach((result) => {
console.log(result.status); // 'fulfilled' または 'rejected'
});
});
Promise.any() - 最初に成功したPromiseを取得
Promise.any()は、最初に成功したPromiseの結果を返します。すべてが失敗した場合のみエラーになります。
const promise1 = Promise.reject('エラー1');
const promise2 = new Promise((resolve) => setTimeout(() => resolve('成功'), 100));
const promise3 = Promise.reject('エラー3');
Promise.any([promise1, promise2, promise3])
.then((value) => {
console.log(value); // '成功'
});
async/awaitとの関係
async/awaitの基本
async/awaitは、Promiseをより読みやすく書くための構文です。asyncキーワードを関数に付けると、その関数は自動的にPromiseを返します。
async function fetchData() {
return 'データ';
}
// 上記は以下と同じ
function fetchData() {
return Promise.resolve('データ');
}
awaitキーワードは、Promiseの結果が返ってくるまで処理を待ちます。awaitはasync関数の中でのみ使用できます。
async function getUserAndPosts() {
try {
const user = await getUser(1);
console.log('ユーザー:', user.name);
const posts = await getPosts(user.id);
console.log('投稿数:', posts.length);
} catch (error) {
console.error('エラー:', error);
}
}
PromiseとAsync/Awaitの使い分け
Promiseチェーンとasync/awaitは、状況に応じて使い分けましょう。
Promiseチェーンが適している場合
- 複数の非同期処理を並列実行したい
- Promise.all()などの静的メソッドを使う
async/awaitが適している場合
- 順次実行する処理が多い
- 同期処理のように読みやすく書きたい
- エラーハンドリングをtry-catchで統一したい
// Promiseチェーン
function processData() {
return fetchUser()
.then(user => fetchPosts(user.id))
.then(posts => processPosts(posts))
.catch(error => handleError(error));
}
// async/await
async function processData() {
try {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
return await processPosts(posts);
} catch (error) {
handleError(error);
}
}
実践例
API通信での利用例
実際のAPI通信では、fetchメソッドがPromiseを返すため、Promiseの理解が重要です。
async function fetchUserFromAPI(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTPエラー: ${response.status}`);
}
const userData = await response.json();
return userData;
} catch (error) {
console.error('API通信エラー:', error);
throw error;
}
}
// 使用例
fetchUserFromAPI(1)
.then(user => {
console.log('ユーザー情報:', user);
})
.catch(error => {
console.error('処理失敗:', error);
});
エラーハンドリングのベストプラクティス
Promiseを使う際は、適切なエラーハンドリングが重要です。
function robustFetch(url) {
return fetch(url)
.then(response => {
// HTTPステータスのチェック
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
// データの検証
if (!data || typeof data !== 'object') {
throw new Error('無効なデータ形式');
}
return data;
})
.catch(error => {
// エラーの種類に応じた処理
if (error.name === 'TypeError') {
console.error('ネットワークエラー:', error.message);
} else {
console.error('その他のエラー:', error.message);
}
throw error; // 呼び出し元でもハンドリングできるよう再スロー
});
}
まとめ
Promiseを使うメリット
Promiseを使うことで、以下のメリットが得られます。
- 非同期処理を直感的に記述できる
- エラーハンドリングが統一的に行える
- コールバック地獄を回避できる
- 複数の非同期処理を柔軟に組み合わせられる