●JavaScriptにおけるPromiseとは
JavaScriptを学んでいく中で、非同期処理という言葉を耳にしたことがあるのではないでしょうか。
非同期処理とは、ある処理が完了するのを待たずに次の処理を進めることができる仕組みのことを指します。
しかし、非同期処理を扱う際には、コールバック地獄などの問題に直面することがありました。
そこで登場したのが、Promise(プロミス)です。
Promiseは、非同期処理を扱うための強力な機能であり、コードの可読性や保守性を向上させることができます。
Promiseを使いこなすことで、より効率的で高度なWebアプリケーションの開発が可能になるでしょう。
○Promiseの基本的な概念と仕組み
Promiseは、非同期処理の状態を表すオブジェクトです。
Promiseには、次の3つの状態があります。
- Pending(保留中) -> 非同期処理が完了していない状態
- Fulfilled(成功) -> 非同期処理が成功した状態
- Rejected(失敗) -> 非同期処理が失敗した状態
Promiseは、new Promise()というコンストラクタを使って生成します。
このコンストラクタには、resolveとrejectという2つの引数を取るコールバック関数を渡します。
const promise = new Promise((resolve, reject) => {
// 非同期処理を記述する
});
非同期処理が成功した場合は、resolve関数を呼び出します。
一方、非同期処理が失敗した場合は、reject関数を呼び出します。
○Promiseの状態とライフサイクル
Promiseのライフサイクルは、次のように進行します。
- Promiseが生成されると、Pendingの状態になります
- 非同期処理が完了すると、resolveまたはrejectが呼び出されます
- resolveが呼び出された場合、PromiseはFulfilledの状態になります
- rejectが呼び出された場合、PromiseはRejectedの状態になります
一度FulfilledまたはRejectedの状態になったPromiseは、その状態を変更することができません。
つまり、Promiseは不変(イミュータブル)なのです。
Promiseの状態に応じて、then、catch、finallyメソッドを使って処理を記述することができます。
promise
.then((result) => {
// 成功時の処理
})
.catch((error) => {
// 失敗時の処理
})
.finally(() => {
// 常に実行される処理
});
thenメソッドは、Promiseが成功した場合に実行される処理を記述します。catchメソッドは、Promiseが失敗した場合に実行される処理を記述します。
finallyメソッドは、PromiseがFulfilledまたはRejectedの状態になった後に、常に実行される処理を記述します。
●resolveとrejectの役割
Promiseを使いこなすためには、resolveとrejectの役割を理解することが重要です。
これらは、Promiseの状態を決定する重要な関数なのです。
resolveは、非同期処理が成功したときに呼び出す関数です。
resolveを呼び出すことで、Promiseの状態はFulfilledになります。
一方、rejectは非同期処理が失敗したときに呼び出す関数です。
rejectを呼び出すと、Promiseの状態はRejectedになります。
これらの関数を適切に使い分けることで、非同期処理の結果に応じた処理を行うことができるようになります。
では、実際のコードを見ながら、resolveとrejectの使い方を詳しく見ていきましょう。
○resolveの使い方とサンプルコード
resolveは、Promiseの中で非同期処理が成功したときに呼び出します。
resolveには、処理の結果を引数として渡すことができます。
この結果は、Promiseのthenメソッドで取得することができます。
○サンプルコード1:Promiseの基本的な使い方
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("非同期処理が成功しました!");
}, 1000);
});
myPromise.then((result) => {
console.log(result);
});
このコードでは、1秒後に”非同期処理が成功しました!”という文字列を返すPromiseを作成しています。
setTimeout関数の中でresolveを呼び出し、成功時のメッセージを引数として渡しています。
そして、Promiseのthenメソッドを使って、resolveで渡された結果を受け取り、コンソールに出力しています。
実行結果
非同期処理が成功しました!
○サンプルコード2:resolveで値を返す例
const countDown = (num) => {
return new Promise((resolve, reject) => {
if (num >= 0) {
setTimeout(() => {
console.log(num);
resolve(num - 1);
}, 1000);
} else {
reject("カウントダウンが終了しました");
}
});
};
countDown(5)
.then((nextNum) => countDown(nextNum))
.then((nextNum) => countDown(nextNum))
.then((nextNum) => countDown(nextNum))
.then((nextNum) => countDown(nextNum))
.then((nextNum) => countDown(nextNum))
.catch((error) => console.log(error));
このコードでは、カウントダウンを行うPromiseを作成しています。
countDown関数は、引数として受け取った数値をコンソールに出力し、1秒後にその数値から1を引いた値をresolveで返します。
countDownが0より大きい場合は、resolveで次のカウントダウンの数値を返し、0以下になった場合はrejectでエラーメッセージを返します。
そして、Promiseチェーンを使って、countDownを連続して呼び出しています。
各Promiseのthenメソッドでは、resolveで返された次のカウントダウンの数値を受け取り、再びcountDownを呼び出しています。
実行結果
5
4
3
2
1
0
カウントダウンが終了しました
○rejectの使い方とサンプルコード
rejectは、Promiseの中で非同期処理が失敗したときに呼び出します。rejectには、エラーオブジェクトや失敗の理由を表す文字列を引数として渡すことができます。
○サンプルコード3:rejectでエラーを返す例
const checkNumber = (num) => {
return new Promise((resolve, reject) => {
if (typeof num === "number") {
resolve(num);
} else {
reject(new Error("引数が数値ではありません"));
}
});
};
checkNumber(123)
.then((result) => console.log(result))
.catch((error) => console.log(error.message));
checkNumber("123")
.then((result) => console.log(result))
.catch((error) => console.log(error.message));
このコードでは、引数が数値であるかどうかをチェックするPromiseを作成しています。
checkNumber関数は、引数が数値の場合はresolveでその数値を返し、数値でない場合はrejectでエラーオブジェクトを返します。
そして、checkNumberを呼び出し、resolveされた場合はthenメソッドで結果を出力し、rejectされた場合はcatchメソッドでエラーメッセージを出力しています。
実行結果
123
引数が数値ではありません
○サンプルコード4:rejectでエラーをキャッチする例
const getUsers = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("ユーザー情報の取得に失敗しました"));
}, 1000);
});
};
getUsers()
.then((users) => console.log(users))
.catch((error) => {
console.error("エラーが発生しました:", error.message);
// エラー処理を行う
// 例:代替データを返す、エラーログを記録する、など
});
このコードでは、ユーザー情報を取得するPromiseを作成していますが、わざと失敗するようにしています。
getUsers関数の中では、1秒後にrejectを呼び出し、エラーオブジェクトを返しています。
そして、getUsers呼び出しのthenメソッドでユーザー情報を取得しようとしますが、rejectされるためcatchメソッドに処理が移ります。
catchメソッドの中では、エラーメッセージをコンソールに出力し、適切なエラー処理を行います。
実行結果
エラーが発生しました: ユーザー情報の取得に失敗しました
このように、rejectを使ってエラーをキャッチし、適切にエラー処理を行うことが重要です。
エラー処理を行うことで、アプリケーションの堅牢性を高めることができます。
●Promiseのチェーン
Promiseを使った非同期処理をより効率的に行う方法の1つに、Promiseチェーンがあります。
Promiseチェーンは、複数の非同期処理を順番に実行し、それぞれの処理結果を次の処理に渡すことができる仕組みです。
Promiseチェーンを使うことで、非同期処理のコードをより読みやすく、保守性の高いものにすることができます。
ただし、Promiseチェーンを使いこなすためには、いくつかのポイントを理解しておく必要があります。
Promiseチェーンを構築するには、Promiseのthenメソッドとcatchメソッドを使います。
thenメソッドは、Promiseが成功したときに実行される処理を定義し、catchメソッドは失敗したときに実行される処理を定義します。
それでは実際に、Promiseチェーンを使ったサンプルコードを見ていきましょう。
○サンプルコード5:thenメソッドを使ったPromiseチェーン
const getUser = (userId) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const user = {
id: userId,
name: 'John Doe',
email: 'john@example.com'
};
resolve(user);
}, 1000);
});
};
const getPosts = (user) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const posts = [
{ id: 1, title: '投稿1', author: user.name },
{ id: 2, title: '投稿2', author: user.name },
{ id: 3, title: '投稿3', author: user.name }
];
resolve(posts);
}, 1000);
});
};
const getComments = (post) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const comments = [
{ id: 1, text: 'コメント1', postId: post.id },
{ id: 2, text: 'コメント2', postId: post.id },
{ id: 3, text: 'コメント3', postId: post.id }
];
resolve(comments);
}, 1000);
});
};
getUser(1)
.then((user) => {
console.log('ユーザー情報:', user);
return getPosts(user);
})
.then((posts) => {
console.log('投稿一覧:', posts);
return getComments(posts[0]);
})
.then((comments) => {
console.log('コメント一覧:', comments);
})
.catch((error) => {
console.error('エラーが発生しました:', error);
});
このコードでは、ユーザー情報を取得し、そのユーザーの投稿一覧を取得し、さらに最初の投稿に対するコメントを取得するという一連の非同期処理を、Promiseチェーンを使って実装しています。
getUser関数でユーザー情報を取得し、そのユーザー情報をthenメソッドで受け取ります。
次に、getPosts関数でそのユーザーの投稿一覧を取得し、さらに次のthenメソッドで受け取ります。
最後に、getComments関数で最初の投稿に対するコメントを取得し、最後のthenメソッドで受け取ります。
実行結果
ユーザー情報: { id: 1, name: 'John Doe', email: 'john@example.com' }
投稿一覧: [
{ id: 1, title: '投稿1', author: 'John Doe' },
{ id: 2, title: '投稿2', author: 'John Doe' },
{ id: 3, title: '投稿3', author: 'John Doe' }
]
コメント一覧: [
{ id: 1, text: 'コメント1', postId: 1 },
{ id: 2, text: 'コメント2', postId: 1 },
{ id: 3, text: 'コメント3', postId: 1 }
]
このように、Promiseチェーンを使うことで、非同期処理の流れを明確に表現することができます。
各thenメソッドでは、前の処理の結果を受け取り、次の処理に渡すことができるため、コードの可読性が向上します。
○サンプルコード6:catchメソッドを使ったエラーハンドリング
const getUser = (userId) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId === 1) {
const user = {
id: userId,
name: 'John Doe',
email: 'john@example.com'
};
resolve(user);
} else {
reject(new Error('ユーザーが見つかりません'));
}
}, 1000);
});
};
const getPosts = (user) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (user.id === 1) {
const posts = [
{ id: 1, title: '投稿1', author: user.name },
{ id: 2, title: '投稿2', author: user.name },
{ id: 3, title: '投稿3', author: user.name }
];
resolve(posts);
} else {
reject(new Error('投稿が見つかりません'));
}
}, 1000);
});
};
getUser(2)
.then((user) => {
console.log('ユーザー情報:', user);
return getPosts(user);
})
.then((posts) => {
console.log('投稿一覧:', posts);
})
.catch((error) => {
console.error('エラーが発生しました:', error.message);
});
このコードでは、getUser関数とgetPosts関数にエラーが発生する条件を追加しています。
getUser関数では、ユーザーIDが1以外の場合にrejectを呼び出してエラーを返します。
getPosts関数では、ユーザーIDが1以外の場合にrejectを呼び出してエラーを返します。
Promiseチェーンの最後にcatchメソッドを追加することで、チェーン内のどの処理でエラーが発生しても、catchメソッドでエラーをキャッチすることができます。
実行結果
エラーが発生しました: ユーザーが見つかりません
このように、Promiseチェーンとcatchメソッドを組み合わせることで、エラーハンドリングをシンプルに実装することができます。
エラーが発生した場合には、catchメソッドに処理が移り、エラーメッセージを出力します。
●Promise.allとPromise.race
Promiseには、複数の非同期処理を扱うための便利なメソッドとして、Promise.allとPromise.raceがあります。
これらのメソッドを使うことで、複数のPromiseを組み合わせて処理することができます。
Promise.allは、複数のPromiseを並列に実行し、すべてのPromiseが成功した場合に、それぞれの結果を配列で返します。
一方、Promise.raceは、複数のPromiseを競争させ、最初に成功または失敗したPromiseの結果を返します。
これらのメソッドを適切に使い分けることで、より効率的で柔軟な非同期処理を実現することができるでしょう。
それでは、実際のサンプルコードを見ながら、Promise.allとPromise.raceの使い方を詳しく見ていきましょう。
○サンプルコード7:Promise.allで複数のPromiseを並列処理
const getUser = (userId) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const user = {
id: userId,
name: `ユーザー${userId}`
};
resolve(user);
}, 1000);
});
};
const getUsers = (userIds) => {
const promises = userIds.map((userId) => getUser(userId));
return Promise.all(promises);
};
getUsers([1, 2, 3])
.then((users) => {
console.log(users);
})
.catch((error) => {
console.error('エラーが発生しました:', error);
});
このコードでは、getUser関数を使って、ユーザーIDからユーザー情報を取得するPromiseを作成しています。
getUsers関数では、userIdsの配列を受け取り、それぞれのユーザーIDに対してgetUserを呼び出し、Promiseの配列を作成します。
そして、Promise.allを使って、すべてのPromiseが成功した場合に、ユーザー情報の配列を返します。
getUsers関数を呼び出した後、thenメソッドでユーザー情報の配列を受け取り、コンソールに出力しています。
実行結果
[
{ id: 1, name: 'ユーザー1' },
{ id: 2, name: 'ユーザー2' },
{ id: 3, name: 'ユーザー3' }
]
Promise.allを使うことで、複数の非同期処理を並列に実行し、すべての処理が完了した後に結果を得ることができます。
これにより、非同期処理の効率を向上させることができます。
○サンプルコード8:Promise.raceで複数のPromiseを競争させる
const getUser = (userId, delay) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const user = {
id: userId,
name: `ユーザー${userId}`
};
resolve(user);
}, delay);
});
};
const getUserFastest = (userIds) => {
const promises = userIds.map((userId, index) => getUser(userId, (index + 1) * 1000));
return Promise.race(promises);
};
getUserFastest([1, 2, 3])
.then((user) => {
console.log(user);
})
.catch((error) => {
console.error('エラーが発生しました:', error);
});
このコードでは、getUser関数に、ユーザーIDに加えて、遅延時間(delay)を渡すようにしています。
getUserFastest関数では、userIdsの配列を受け取り、それぞれのユーザーIDに対してgetUserを呼び出し、Promiseの配列を作成します。
このとき、遅延時間は、インデックスに応じて異なる値を設定しています。
そして、Promise.raceを使って、最初に成功したPromiseの結果を返します。
getUserFastest関数を呼び出した後、thenメソッドで最初に成功したPromiseの結果(ユーザー情報)を受け取り、コンソールに出力しています。
実行結果
{ id: 1, name: 'ユーザー1' }
Promise.raceを使うことで、複数の非同期処理を競争させ、最初に成功したPromiseの結果を得ることができます。
これにより、複数の非同期処理のうち、最も早く完了した処理の結果を利用することができます。
●Promiseとasync/await
Promiseを使った非同期処理をより簡潔に記述する方法として、async/awaitがあります。
async/awaitは、ES2017で導入された機能で、Promiseをベースにした非同期処理を同期的に記述できるようにするものです。
async/awaitを使うことで、Promiseのthenやcatchメソッドを使わずに、非同期処理の結果を待ち受けることができます。
これにより、コードの可読性が向上し、よりシンプルで理解しやすい非同期処理を実装することができるでしょう。
ただし、async/awaitを使う際には、いくつか注意点があります。
例えば、async/awaitを使った関数は常にPromiseを返すため、関数の戻り値を適切に処理する必要があります。
それでは、実際のサンプルコードを見ながら、async/awaitの使い方を詳しく見ていきましょう。
○サンプルコード9:async/awaitを使った非同期処理の例
const getUser = (userId) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const user = {
id: userId,
name: `ユーザー${userId}`
};
resolve(user);
}, 1000);
});
};
const getPosts = (userId) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const posts = [
{ id: 1, title: '投稿1', author: userId },
{ id: 2, title: '投稿2', author: userId },
{ id: 3, title: '投稿3', author: userId }
];
resolve(posts);
}, 1000);
});
};
const getPostsForUser = async (userId) => {
try {
const user = await getUser(userId);
const posts = await getPosts(user.id);
return posts;
} catch (error) {
console.error('エラーが発生しました:', error);
return [];
}
};
getPostsForUser(1)
.then((posts) => {
console.log(posts);
});
このコードでは、getUser関数とgetPosts関数を使って、ユーザー情報と投稿一覧を取得するPromiseを作成しています。
getPostsForUser関数では、async/awaitを使って、これらのPromiseを同期的に処理しています。
getPostsForUser関数は、async関数として定義されています。関数内では、try-catch文を使ってエラーハンドリングを行っています。
まず、await getUser(userId)でユーザー情報を取得し、次にawait getPosts(user.id)で投稿一覧を取得しています。
これらの非同期処理の結果は、変数userとpostsに順番に代入されます。
最後に、getPostsForUser関数を呼び出し、thenメソッドで投稿一覧を受け取ってコンソールに出力しています。
実行結果
[
{ id: 1, title: '投稿1', author: 1 },
{ id: 2, title: '投稿2', author: 1 },
{ id: 3, title: '投稿3', author: 1 }
]
○サンプルコード10:async/awaitとtry/catchの例
const getUser = (userId) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId === 1) {
const user = {
id: userId,
name: `ユーザー${userId}`
};
resolve(user);
} else {
reject(new Error('ユーザーが見つかりません'));
}
}, 1000);
});
};
const getPosts = (userId) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId === 1) {
const posts = [
{ id: 1, title: '投稿1', author: userId },
{ id: 2, title: '投稿2', author: userId },
{ id: 3, title: '投稿3', author: userId }
];
resolve(posts);
} else {
reject(new Error('投稿が見つかりません'));
}
}, 1000);
});
};
const getPostsForUser = async (userId) => {
try {
const user = await getUser(userId);
const posts = await getPosts(user.id);
return posts;
} catch (error) {
console.error('エラーが発生しました:', error.message);
return [];
}
};
getPostsForUser(1)
.then((posts) => {
console.log('ユーザー1の投稿:', posts);
});
getPostsForUser(2)
.then((posts) => {
console.log('ユーザー2の投稿:', posts);
});
このコードでは、getUser関数とgetPosts関数にエラーが発生する条件を追加しています。
getUser関数では、ユーザーIDが1以外の場合にrejectを呼び出してエラーを返します。
getPosts関数では、ユーザーIDが1以外の場合にrejectを呼び出してエラーを返します。
getPostsForUser関数内では、try-catch文を使ってエラーをキャッチしています。
エラーが発生した場合には、catch節でエラーメッセージをコンソールに出力し、空の配列を返しています。
getPostsForUser関数を呼び出す際に、ユーザーID 1と2の場合で結果を確認しています。
実行結果
ユーザー1の投稿: [
{ id: 1, title: '投稿1', author: 1 },
{ id: 2, title: '投稿2', author: 1 },
{ id: 3, title: '投稿3', author: 1 }
]
エラーが発生しました: ユーザーが見つかりません
ユーザー2の投稿: []
●Promiseを使う際のベストプラクティス
Promiseを使った非同期処理を効果的に実装するためには、いくつかのベストプラクティスを踏まえることが重要です。
ここでは、Promiseを使う際に気をつけるべきポイントと、コードの可読性や保守性を高めるためのテクニックを紹介します。
後述するベストプラクティスを理解し、適切に実践することで、より優れたPromiseを使った非同期処理のコードを書くことができるでしょう。
それでは、具体的に見ていきましょう。
○Promiseを使うタイミングと注意点
Promiseを使うべきタイミングは、主に非同期処理を扱う場合です。
例えば、ファイルの読み込み、ネットワークリクエスト、データベースへのクエリなどは、Promiseを使って非同期処理を実装するのに適しています。
ただし、Promiseを使う際には、いくつか注意点があります。
まず、Promiseは一度生成されると、そのPromiseの状態は変更できません。
つまり、一度resolveまたはrejectが呼び出されると、Promiseの状態は確定します。
また、Promiseのコンストラクタ内で例外が発生した場合、そのPromiseは自動的にRejectedの状態になります。
したがって、Promiseのコンストラクタ内では、適切にエラーハンドリングを行う必要があります。
○Promiseのネストを避ける
Promiseを使う際には、Promiseのネストを避けることが重要です。
Promiseのネストが深くなると、コードの可読性が下がり、保守性も低下します。
これは、いわゆる「コールバック地獄」と同様の問題です。
Promiseのネストを避けるためには、Promiseチェーンを活用することが効果的です。
Promiseチェーンを使うことで、複数の非同期処理を順番に実行し、コードの流れを明確に表現することができます。
例えば、次のようなPromiseのネストを避け、Promiseチェーンを使って書き換えることができます。
ネストされたPromise(非推奨)
getUser(userId)
.then((user) => {
getPosts(user.id)
.then((posts) => {
getComments(posts[0].id)
.then((comments) => {
console.log(user, posts, comments);
});
});
});
Promiseチェーンを使った例(推奨)
getUser(userId)
.then((user) => {
return getPosts(user.id);
})
.then((posts) => {
return getComments(posts[0].id);
})
.then((comments) => {
console.log(user, posts, comments);
});
このように、Promiseチェーンを使うことで、コードの可読性と保守性を向上させることができます。
○適切なエラーハンドリング
Promiseを使う際には、適切なエラーハンドリングを行うことが重要です。
Promiseのエラーハンドリングには、主にcatchメソッドを使います。
catchメソッドは、Promiseチェーン内のいずれかの処理で発生したエラーをキャッチし、適切に処理することができます。
エラーが発生した場合には、エラーメッセージをログに出力したり、ユーザーに適切なメッセージを表示したりするなどの処理を行います。
ここでは、catchメソッドを使ったエラーハンドリングの例です。
getUser(userId)
.then((user) => {
return getPosts(user.id);
})
.then((posts) => {
return getComments(posts[0].id);
})
.then((comments) => {
console.log(user, posts, comments);
})
.catch((error) => {
console.error('エラーが発生しました:', error);
// エラー処理を行う
});
catchメソッドを使うことで、Promiseチェーン内のエラーを一箇所で処理することができます。
エラーハンドリングを適切に行うことで、アプリケーションの堅牢性を高めることができます。
また、async/awaitを使う場合には、try-catch文を使ってエラーハンドリングを行います。
ここでは、async/awaitとtry-catch文を使ったエラーハンドリングの例を紹介します。
async function getUserPostsComments(userId) {
try {
const user = await getUser(userId);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
console.log(user, posts, comments);
} catch (error) {
console.error('エラーが発生しました:', error);
// エラー処理を行う
}
}
このように、async/awaitを使う場合でも、try-catch文を使って適切にエラーハンドリングを行うことができます。
まとめ
本記事では、JavaScriptのPromiseについて、その基本的な概念から、resolveとrejectの使い方、Promiseチェーン、Promise.allとPromise.race、async/awaitとの関係、そしてベストプラクティスまで、様々な角度から詳しく解説してきました。
Promiseは、非同期処理を扱う上で非常に重要な機能であり、これを理解することで、より効率的で可読性の高いコードを書くことができるようになります。
特に、resolveとrejectの適切な使い分け、Promiseチェーンを活用したコードの整理、async/awaitを組み合わせた非同期処理の記述など、実務で役立つテクニックを数多く紹介しました。
Promiseを適切に使いこなすことで、より高度で洗練されたWebアプリケーションの開発に挑戦できるはずです。