はじめに
JavaScriptの非同期処理について理解するときに、避けて通れないのが「Promise」です。
今まで雰囲気で何となく実装してきていたので、今回基礎からしっかり理解します。
Promiseとは
Promiseは非同期処理の完了、もしくは失敗を表すオブジェクトです。
処理の完了および失敗に対して、それぞれ処理を紐づけることができます。
非同期処理は結果を返すことができますが、即座に返すことができません。
その代わり、未来のある時点(処理の結果が確定し、成功/失敗が明らかになった時)で値を提供するオブジェクトを返すことで、同期処理と同じように値を返すことができるようになります。
この値を提供するオブジェクトがPromiseになります。
実際のコード例から、さらにPromiseを深堀りします。
補足:Promiseの状態
Promiseオブジェクトは、以下の3つの状態のいずれかを持ちます。
- pending(保留中): 初期状態。処理が完了していない状態
- fulfilled(成功): 処理が成功して完了した状態
- rejected(拒否): 処理が失敗して完了した状態
状態はpendingから始まり、fulfilledまたはrejectedに変化します。
一度確定したら状態が変更されることはありません。
実際にコードを書いていてこの状態を目にすることはありませんが、内部ではこの状態によって挙動が決まっているということは覚えておくとよいです。
Promiseを生成する
基本的に自身でPromiseを生成することはあまりないのですが、実際に処理を書くことでより詳しく中身を理解していきます。
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
// APIリクエストをシミュレート
setTimeout(() => {
if (userId > 0) {
// 成功: ユーザーデータを返す
resolve({
id: userId,
name: `User ${userId}`,
email: `user${userId}@example.com`,
});
} else {
// 失敗: エラーを返す
reject(new Error("Invalid user ID"));
}
}, 1000); // 1秒後に結果を返す
});
}
fetchUserData
という処理がPromiseを返す関数となるよう設定しました。
コンストラクタ
Promiseの生成にはnew
キーワードとPromiseコンストラクタの呼び出しが必要です。
executor関数
コンストラクタには引数として関数を設定します。
この関数はexecutor関数と呼ばれ、この関数内で非同期処理を実行します。
resolveとreject
executor関数は引数として2つの関数を設定します。
第一引数の関数は、非同期処理が成功した場合に呼び出す関数で、慣例的にresolveとすることが多いです。
第二引数の関数は、非同期処理が失敗した場合に呼び出す関数で、慣例的にrejectとすることが多いです。
fetchUserData
の解説
今回はexecutor関数の処理として、仮想のAPI処理を作成しています。
タイムアウトの後、ユーザーデータを返す処理を定義しました。
非同期処理の成功/失敗はuserId
の分岐で決定しています。
0より大きい場合には処理の成功として、resolveにユーザーデータのオブジェクトを設定しています。
それ以外の場合には処理の失敗として、rejectにエラーを設定しています。
この実装からわかること
返却されるPromiseオブジェクトは、処理の成功(resolve)と失敗(reject)のどちらの状態も保持しています。
まだこの時点で処理は実行されていないので、どちらになるかはわかりません。
しかし、成功時にはresolveの引数が、失敗時にはrejectの引数が値として設定されることになります。
このように、未来のある時点で値を提供するオブジェクトがPromiseです。
Promiseを利用する
生成したPromiseを利用する方法を改めて確認し、さらにPromiseについての理解を深めます。
const userData = fetchUserData(1);
userData
.then((user) => {
console.log("ユーザーデータを取得しました:", user);
})
.catch((error) => {
console.error("エラーが発生しました:", error.message);
});
userData
でPromiseオブジェクトを受け取ります。
Promiseオブジェクトにはthen
、catch
、finally
のメソッドが用意されています。
finally
については今回は割愛します。
then
引数として関数を設定します。
この関数はPromiseオブジェクトがresolveしたときに実行される処理となります。
戻り値として新しいPromiseオブジェクトを返します。
また、この関数は引数としてresolveに指定した値が設定されます。
補足:thenメソッドの第二引数
thenは実際には2つめの引数を設定することができます。
promise.then(onFulfilled, onRejected)
onFulfilled
は上記で解説したresolveしたときに実行される処理です。
onRejected
はrejectされたときに実行される処理となります。
これにより、1つのthenメソッドで成功と失敗の両方のケースを処理できます。
ただし、可読性の観点から、多くの場合は後述のcatchメソッドを使用してエラーハンドリングを行うことが推奨されます。
catch
引数として関数を設定します。
この関数はPromiseオブジェクトがrejectされたときに実行される処理となります。
こちらも戻り値として新しいPromiseオブジェクトを返します。
また、この関数は引数としてrejectに指定した値が設定されます。
この実装からわかること
実行結果の確定していないuserData
というPromiseオブジェクトに対して、rosolveしたときの処理をthen
で、rejectされたときの処理をcatch
で指定しています。
これが冒頭のPromiseとはで触れていた
処理の完了および失敗に対して、それぞれ処理を紐づける
ということになります。
補足:Promiseの連鎖
then
メソッドは新しいPromiseを返すため、複数の非同期処理を連鎖させることができます。
fetchUserData(1)
.then(user => {
console.log("ユーザーデータを取得しました:", user);
return fetchUserPosts(user.id); // 新しいPromiseを返す
})
.then(posts => {
console.log("ユーザーの投稿を取得しました:", posts);
return fetchComments(posts[0].id); // さらに新しいPromiseを返す
})
.then(comments => {
console.log("最初の投稿のコメントを取得しました:", comments);
})
.catch(error => {
console.error("エラーが発生しました:", error.message);
});
ポイントは、必ずthen
メソッドの中でreturn
を使って値を返すことです。
そうしないと、後続の処理で値を使えなくなってしまいます。
この方法により、複数の非同期操作を順序立てて実行し、各ステップの結果を次のステップで利用することができます。
なお、エラーは最後のcatchで一括して処理されます。
まとめ
Promiseについてその実態を理解しないまま、なんとなくthen
やchach
を見様見真似で使ってきていましたが、その挙動を紐解くことで、何をしていたのかをようやく理解することができました。
非同期処理はまだまだ苦手意識があるので、こういったところから少しずつ克服していきたいと思いました。