これはnote.comに書いた記事の再掲です。
JavaScript の構文は一通り学んだ、Promiseもfetch, axios, async/awaitの構文も一応は習った、Promiseがレストランで頼んだ料理がまだ来ていない状態と似ているという例え話も聞いたし、そんなことは分かってる、だけど、結局もやもやしていて、苦手意識が続く…それが私でした。その後、実装のイメージをつかめばもやもやが解消できると分かるまでに相当時間がかかりましたが、同じ思いを持っていらっしゃる人の1人にでも役に立てばという思いで、Promise の実体、仕組みを例え話なしで解説しました。
1 Promise オブジェクト
new Promise() という形から分かる通り、これで作っているのはオブジェクト(インスタンス)です。でも、そのオブジェクト、どこにできてるの?オブジェクトの操作とか、コードの中には全然出てこないじゃん?!と思うかもしれません。
なぜコード自体に出てこないかというと、Promise オブジェクト自体はブラウザに組み込まれた JavaScriptエンジン の中に実装されていて、コードの中では最小限のことさえ書けば、あとは JavaScript エンジン側で処理してくれるよう作られているから。
2 JavaScript って私たちが書くコードの外に回っているものがある
JavaScript を習うというと、当然、コードの書き方を習うけど、私たちが書くコードはそれだけで動いているわけじゃなくて、ブラウザの中に組み込まれた JavaScript エンジンや、ブラウザ自体の機能におんぶにだっこしている。JavaScript プログラムで使われ、かつ、ハードウェアや通信に近い機能は JavaScript エンジンというプログラムにまとめられて、それがブラウザに組み込まれている。そして、JavaScript に限らずどんなプログラムでも必要となる機能はブラウザ本体に入っています。
で、JavaScript という言語を機能拡張するということは、それができるように JavaScript エンジンが機能拡張されるということで、2015年にPromise (と一緒に.then()がやら.catch()やら)が登場したときも、それを使えるように JavaScript エンジンが拡張されたという意味だった。
とにかく、私たちが書く JavaScript コードは JavaScript エンジンとかブラウザとかに依存しているんだけど、ときどき、コードの中でそういった機能を使う場面があって、どうやってそういうのにアクセスしたらいいか決まってる。
たとえば、コードの中で、document と書くと、別に定義しなくても(というか、もう決まってるから改めて定義したらだめ)それはコンピュータが読み込んだ HTML に基づいてメモリ中に作ったDOMツリーのことだし、window はざっくり言えば、ブラウザ画面に映っているエリアを指すオブジェクト。指定の時間が経つと指定した関数を実行する setTimeoutという関数はブラウザに組み込まれている機能だったりする。
Promise オブジェクトは JavaScript エンジンの中に組み込まれていて、コードで非同期処理を書きたいときに使いたい機能が便利にまとめられている。
では、Promiseとは?なんだけど、その前に、そもそも、非同期処理とは?
3 そもそも、非同期処理ってなに。
JavaScript はシングルスレッドの言語。つまり、JavaScriptで書かれたプログラムは、コンピュータのCPU中の一つのコア(それ単体で使える最小単位の計算回路)内で処理される。
マルチコア CPU 搭載!なんていって、複数のコア(計算回路)が入っているCPUを使っているパソコン(最近じゃこれが普通)であっても、JavaScript のプログラム自体は一番上の行から順番にザーーーッと一つのコアだけを使って実行される。
とはいえ、プログラムの中にはそのコアの計算だけじゃ完結しない処理もある。例えば、ネットワークを越えて外部のサービスに問い合わせてデータを取ってくるとか。そもそも通信は JavaScript が実行されているコアじゃなくて、ブラウザの機能に任せることになるし、レスポンスが返ってくるのに多少の時間はかかる。
なので、次に書いてある処理に通信して取ってくるデータが必要なのに、それを待たずに進めてしまうと間違った結果になったり、エラーが出てしまう。といって、そのデータが来るまでコアの計算を止めてしまうと、プログラム全体がその間、停止してしまう。
そこで、プログラムの中で
(1)どんどん進めてよい処理と、
(2)実行をあるタイミングまで待つ処理
を分けて(1)は走らせておいて、(2)だけ別扱いにして(=非同期にして)待たせておいて、しかるべきタイミングで処理を実行する。これが非同期処理。
4 コールバック地獄って、なに?
Promise を習うとき、「初期の JavaScript では非同期処理を書くために、コールバックを使ったんだけど、それだとコールバック地獄っていうくらいコードが読みにくくなってたんでPromiseが作られた」と説明され、教材だと、コールバック地獄とはこんな入れ子になってるコードだよと書いてある。
◎◎◎◎◎◎◎◎◎
△△△△△△△△△
〇〇〇〇〇〇〇〇〇
◇◇◇◇◇◇◇◇◇
◇◇◇◇◇◇◇◇◇
〇〇〇〇〇〇〇〇〇
△△△△△△△△△
◎◎◎◎◎◎◎◎◎
まず、コールバック関数とは?
関数に引数として渡される関数のこと。
もう少していねいに言うと
関数 A に関数 B を引数として渡して、
A の処理の内側で B を呼び出すようにするとき、
呼び出される関数という意味で B を「コールバック関数」と呼ぶ。
じゃ、非同期処理を書くのにコールバック関数をどう使うか?というと、特定のタイミングまで待ってから実行したい関数を「特定のタイミングになったら処理を実行するタイプの関数」に渡す。
当たり前だけど、いくらコールバックにしても、フツー、つまり同期の関数に渡しても、非同期にはならないからね!(ここだけの話🤫私は最初、ここで混乱した。)
とにかく、非同期、つまり、特定のタイミングになって初めて、渡された関数を実行する関数が JavaScript には用意されてて、決まった関数名を使えば、すぐ使えるように、JavaScript エンジンやブラウザに実装されている。
例えば、そんなタイプの関数として setTimeout() があって、これは指定した時間が経ったタイミングで引数に渡された関数を実行してくれる。
超簡単な関数 sayHello を作ってこれを setTimeout() に渡したのが下の例。
// sayHello() の定義
const sayHello = function () {
console.log("Hello");
};
// setTimeout() に sayHello とタイミング(単位:ミリ秒)を渡す
setTimeout(sayHello, 3000); // 3000ミリ秒後に実行する
あと、引数として渡す形ではないけど、下のようなパターンもある。これも特定のタイミングで関数を実行するので、広義のコールバック。
xhr.onload = // この後に関数を書いて、xhr.onload に代入する
これは、XMLHttpRequest というブラウザに実装されている機能を使って通信するときに出てくる。(とはいえ、XMLHttpRequest はもっと便利な fetch が登場してから使われることが少なくなったけど。)
例えば、下のコードでは
(1)まず、通信のためのオブジェクトを作り、
(2)それにリクエストタイプ(GETとか)とURLを設定して、
(3)そのオブジェクトの onload プロパティに「通信が完了したら実行したい関数」を代入する。(これが広義のコールバック)
(4)オブジェクトのメソッド .send() でリクエストを送る。
通信が完了したタイミングで、onload に代入された関数が実行される。
const xhr = new XMLHttpRequest(); // (1) オブジェクトを作ってxhrという変数に入れている。
xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/1"); // (2)
// ここから (3)。function(){...} を代入してる。
xhr.onload = function(){
const response = xhr.responseText; // レスポンスから JSON を抽出する。
const data = JSON.parse(response); // JSON を JavaScript のオブジェクトに変換
console.log(data); // オブジェクトをコンソールに表示
};
xhr.send(); // (4)
( コード中の "https://jsonplaceholder.typicode.com/todos/1" はリクエストを投げてレスポンスを返させるテストでよく使われる URL。)
通信が完了すると xhr.onload に代入された関数が実行され、レスポンスがオブジェクトとしてコンソールに表示される。
さて、上のコード例を見ても、え?コールバック地獄じゃないじゃないか。普通にきれいに並んで全然読みやすい、と思うかもしれない。
きれいな読みやすい形というのはこういうイメージで言ってる:
AAAAAAAAA
BBBBBBBBB
CCCCCCCCC
DDDDDDDDD
確かに。
でも、これくれいだったら読みにくくないけど、コールバック地獄と言っているのは、非同期処理が連鎖するときの話。
非同期処理は結構、連鎖する。
例えば「ブラウザの機能を使って今いる場所の緯度経度を調べ、それを天気予報を教えてくれるサーバーに送って予報情報をレスポンスとして受け取り、それに合う適切な画像を読み込んで表示する」ときは各ステップが非同期処理。
これをコールバックで表そうとすると、特定のタイミングまで処理を待つ関数を入れ子にして、だいたいこんな形になる。
navigator.geolocation.getCurrentPosition(function (position) { 【1】
...
xhr.onload = function () { 【2】
...
img.onload = function () { 【3】
...
};
img.src = imgUrl; 【4】
};
xhr.send(); 【5】
});
実際のコード全体はこちら。(上の各行が番号を示しています。)
// navigatorは、多様なデータを提供するブラウザの機能にアクセスする、JavaScriptのオブジェクト。
// これを以下のように使うと今のブラウザの位置情報が得られ、
// コールバック関数の引数に自動的に位置情報が渡される。
navigator.geolocation.getCurrentPosition(function (position) { // ➡【1】
// 引数に渡された位置情報から、緯度、経度を取り出す。
const lat = position.coords.latitude;
const lon = position.coords.longitude;
// リクエストを作る。
const xhr = new XMLHttpRequest();
// 緯度経度に基づいて天気予報を返してくれるURLに、分かった緯度経度を入れた、
// リクエストを送るよう設定する。
xhr.open(
"GET",
`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true`,
);
// 実際にリクエストが送られ、レスポンスが返ってきたら、
// そのレスポンスがJSONなのでパースしてオブジェクトに変換し、そこから天気コードを
// 抽出する関数が走るよう設定する。// ➡【2】
xhr.onload = function () {
const data = JSON.parse(xhr.responseText);
const code = data.current_weather.weathercode;
// 返ってくる天気コードが0の時は晴れ、3の時は雨、その間は曇りなので、
// それに基づいて、雨・晴れ・曇りの画像を返してくれるURLをimgUrlに設定する。
let imgUrl = "https://ssl.gstatic.com/onebox/weather/64/rain.png";
if (code === 0) {
imgUrl = "https://ssl.gstatic.com/onebox/weather/64/sunny.png";
} else if (code >= 1 && code <= 3) {
imgUrl = "https://ssl.gstatic.com/onebox/weather/64/partly_cloudy.png";
}
// image要素を作る。
const img = document.createElement("img");
// 画像が読み込まれたら、bodyにその要素をアペンドするよう設定する。// ➡【3】
img.onload = function () {
document.body.appendChild(img);
};
// imgUrlをimg のsrcに代入。➡画像が読み込まれる。 // ➡【4】
img.src = imgUrl;
};
// リクエストを送る。(その結果、リクエストが送られ、レスポンスが返ってくる。)// ➡【5】
xhr.send();
});
それぞれ、タイミングを待って実行する仕組みに入れ子にしています。そうしないと、前も触れた通り、上からザッと一瞬で実行され、前の処理が終わる前に次の処理が行われてしまって、間違った結果になったり、エラーになったりします。
コールバック地獄は入れ子になってて読みにくいというだけではなくて、途中、エラーが発生したときの対応も各ステップで書かないといけない。これが面倒。
実際のコードでそれをするために★印の箇所を追加したのがこちら。
<script>
// 位置情報の取得
navigator.geolocation.getCurrentPosition(
// 【位置情報:成功したときの処理】
function (position) {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
const xhr = new XMLHttpRequest();
xhr.open(
"GET",
`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true`
);
// 【API通信:レスポンス受信したときの処理】
// 【API通信:レスポンス受信したが、通信ステータスコードが400,500番台のときの処理】★
xhr.onload = function () {
if (xhr.status !== 200) {
console.error("API通信エラー - ステータス:", xhr.status);
return;
}
// 【API通信:レスポンス受信が正常に行われたときの処理】
// try catchでパース成功、失敗時の処理を書く。★
try {
const data = JSON.parse(xhr.responseText);
const code = data.current_weather.weathercode;
let imgUrl = "https://ssl.gstatic.com/onebox/weather/64/rain.png";
if (code === 0) {
imgUrl = "https://ssl.gstatic.com/onebox/weather/64/sunny.png";
} else if (code >= 1 && code <= 3) {
imgUrl = "https://ssl.gstatic.com/onebox/weather/64/partly_cloudy.png";
}
const img = document.createElement("img");
img.onload = function () {
document.body.appendChild(img);
};
// 【画像読み込み:失敗したときの処理】★
img.onerror = function () {
console.error("画像の読み込みに失敗しました:", imgUrl);
};
img.src = imgUrl;
// パース失敗時の処理 ★
} catch (e) {
console.error("JSON解析エラー:", e);
}
};
// 【API通信:失敗したときの処理】★
xhr.onerror = function () {
console.error("ネットワークエラーが発生しました");
};
xhr.send();
},
// 【位置情報:失敗したときの処理】★
function (error) {
console.error("位置情報の取得失敗:", error.message);
}
);
</script>
細かな書き方は後で説明するけど、Promiseを使うとコードの形は、Promiseで「最初こうする」を書き、.then()で「それができたら次こうする」と連ねて書くので、下のような構造になる。
何が嬉しいって、処理を上から処理Aをして、次に処理Bをして、と順番に読んで行ける。入れ子になってるときは、処理Aを記述して、その次に他の処理の記述をし、最後に処理Aを実行するというコードだった。
エラーハンドリングも各ステップごとに書かず、どの段階でエラーになっても一番下の catch さえ書けばエラーハンドリングできるので、ずいぶん楽!
<script>
// 1. 位置情報を取得する
new Promise((resolve, reject) => {
})
// 2. 位置情報をもとに、XMLHttpRequest で API 通信で天気データをとってくる
.then((position) => {
})
// 3. データをもとに適切なお天気画像をとってきて、読み込む
.then((data) => {
})
// 4. 読み込んだ画面に描画する
.then((img) => {
})
// エラーハンドリング
.catch((error) => {
})
</script>
こちらが実際のコード。長いけれど、上から順に処理が並んでる。
<script>
// 1. 位置情報を取得
new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject);
})
// 2. XMLHttpRequest で API 通信
.then((position) => {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(
"GET",
`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true`,
);
xhr.onload = () => {
if (xhr.status === 200) {
try {
const data = JSON.parse(xhr.responseText);
resolve(data);
} catch (e) {
reject(new Error("JSONのパースに失敗しました"));
}
} else {
reject(new Error(`APIエラー HTTPステータス: ${xhr.status}`));
}
};
xhr.onerror = () =>
reject(new Error("ネットワークエラーが発生しました"));
xhr.send();
});
})
// 3. 受け取ったデータをもとに画像をとりよせ、事前読み込み
.then((data) => {
const code = data.current_weather.weathercode;
let imgUrl = "https://ssl.gstatic.com/onebox/weather/64/rain.png";
if (code === 0) {
imgUrl = "https://ssl.gstatic.com/onebox/weather/64/sunny.png";
} else if (code >= 1 && code <= 3) {
imgUrl = "https://ssl.gstatic.com/onebox/weather/64/partly_cloudy.png";
}
return new Promise((resolve, reject) => {
const img = document.createElement("img");
img.onload = () => resolve(img);
img.onerror = () =>
reject(new Error("画像の読み込みに失敗しました: " + imgUrl));
img.src = imgUrl;
});
})
// 4. 画面に描画
.then((img) => {
document.body.appendChild(img);
})
// エラーハンドリング
.catch((error) => {
console.error("エラーが発生しました:", error.message);
});
</script>
めでたしめでたし。
この例は、各ステップが非同期処理だったけど、前の処理の完了を待つ処理があるなら、待っている処理自体が同期処理(コアの中ですぐにできる処理)であっても、.then() に入れて、前の処理が完了してから実行させる。例えば、通信で JSON データをとってきて JavaScript のオブジェクトに変換するときには、(JSON からオブジェクトへの変換は単体で行えば同期処理なんだけど)通信の完了を待ってから変換したいので.then()の中に入れたりする。
では、その Promise の実体はどんなもので、どうやって使うのか?
5 Promiseオブジェクト?
その役割は「〇〇を行って、それが完了したら、△△という値を引数としてXXを実行する」こと。
実体としてはオブジェクトで、new Promise() とすると、メモリ上にPromise オブジェクトができるように JavaScriptエンジンの中で実装されている。
このオブジェクトを私たちが直接見ることはフツーない。(下の概念図で★印がついているプロパティは、ブラウザベンダーが私たちの便宜のためコンソールで見えるようにしてくれてる。)
Promiseオブジェクトの概念図はこちら。プロパティとそれぞれの最初の値はこうなってる。
{
state: "pending", // ★ 状態。最初はペンディング
result: undefined, // ★ 処理が完了したら、ここにデータが入る。
fulfilledCallbacks: [], // 処理の完了を待っている関数を格納する配列
rejectedCallbacks: [], // エラーハンドラ関数を格納する配列
};
コンソール上で処理がからっぽのpromiseを作ってみると、ブラウザが state と result を見せてくれる。
Promise の作り方をざっくり言うと、
まず、new Promise() の中に:
- 何を行うかを書く。
- それがうまく完了したら、resolve() が呼び出されるように書いて、
その引数に次の処理に渡す値を渡す。 - 上手くいかなかったら、reject() が呼ばれるように書いて、
その引数として投げたいエラーを書く。
例:reject(new Error("エラーが発生しました"))
こうすると、promise オブジェクトが作られる。
こちらがコンソールで試した例。この例では(強制的に)resolve() が実行されるように書かれているので、result に "Success!" が入っている。
さらに promise に続けて .then()の中に:
4. データを受け取って次に何をするかを書く。
.then()に書いた処理が実行されている。
6 resolve と reject
では、少し細かく見ていこう。まず new Promise()の中はこんな関数になっている。
(resolve, reject)=>{}
resolve, rejectって何をするの???定義してないし、いきなり何?!って思いそうだけど(また告白だけど、私は思った!)、実は resolve, reject という名前はなんでもよくて(!)第1引数、第2引数という順番が大事なのがJavaScript。
実は、この2つの引数は、特定の機能を持つ関数として、あらかじめ JavaScript エンジンの中で実装されている。その機能とは以下の通り。
第1引数の関数(一般に resolve とされる関数)
1. promise の state を fulfilled に変換する。
2. この関数(resolve)の引数を result に代入する。
3. fulfilledCallbacksに登録されている関数に result の値を渡して実行(※)する。
第2引数の関数(一般に reject とされる関数)
1. promise の state を rejected に変換する。
2. この関数の引数(普通はerrorを入れる)を result に代入する。
3. rejectedCallbacksに登録されているエラーハンドラに、resultに入っているエラーを渡して実行(※)する。
こんな風にJavaScriptエンジンの方でややこしい部分を全部肩代わりしてくれていうので、私たちは…
1.処理が上手くいったら 第1引数と同じ名前の関数が、
2.上手くいかなかったら 第2引数と同じ名前の関数が呼ばれる
ように書き、あとは
それぞれに適切な引数を渡せばいいだけ。
そして、resolve, rejectといった名前は本当はなんでもいいんだけど、その機能を反映して resolve、rejectという名前が慣習的に使われる。
先に示した画像と同じだけど、promise ができているのが、ブラウザのコンソールで分かる。(ちなみに、ブラウザはcallbacks の配列までは見せてくれない。)
※ 正確には、関数はその場ですぐ実行されるのではなく、コンピュータの中のメモリ上に、このプログラムが走っている間だけ、こういうコールバック用に設けられる順番待ちの列に並び、今実行中の処理が全部終わったタイミングでこの行列から取り出されて実行される。
だけど今の段階ではホントーに無視していい話です。
よくPromiseの解説でマイクロキュー(=この話)が出てきますが、それを理解しなくても、非同期処理は使えるし、 Promise の理解だけでも脳は大変なので、それは別のトピックとして学習すればいいと思います。
7 次の処理の登録
「promise に書いた処理が完了したら、次に実行される関数」はどこに書くかというと、.then() に書くことで fulfilledCallbacks配列に登録できる。
こうすると、処理が成功するとresolve() が呼ばれる ➡ promiseがfulfilled状態になる。➡ fulfilledCallback配列にある関数が実行される。
const p = new Promise((resolve, reject) => {
// ここに非同期処理のコードがあると想定。
let success = true; //ここでは非同期処理が成功したと想定。
if (success) {
resolve("Success!");
} else {
reject(new Error("Error has occurred"));
}
});
// p の処理が上手くいったら次に実行したい関数を、.then()を使って
// p の fulfilledCallbacks に関数を登録する。
p.then((result) => {
console.log(result);
});
// コンソールにSuccess!と表示される。
これで、promiseの「〇〇を行って、それが完了したら、△△という値を引数としてXXを実行する」ところまでができた!
8 .then() も promise を返す。➡ Promiseチェーン
今まで見たように、new Promise() とすると promise オブジェクトが返ってくるんだけど、.then() でも promise オブジェクトが返ってくる。ということは、それに.then() を続けて、さらに次の処理を登録できる。
以下に p1 という promise に .then() を使って p2 を作り、それに .then()を使
って p3 を作るというコードを書いてみた。
const p1 = new Promise((resolve, reject) => {
// ここに非同期処理のコードがあると想定。
let success = true; //ここでは非同期処理が成功したと想定。
if (success) {
resolve("Success!");
} else {
reject(new Error("Error has occured"));
}
});
const p2 = p1.then((result) => {
//この関数 ➡ p1 の fulfilledCallbacks に登録。
return result + "!!"; //この関数が完了すると p2 の resultにこの戻り値が入り、
}); //result の値を渡して p2 のfulfilledCallbacks に登録された関数が実行される。
const p3 = p2.then((result) => {
//この関数が p2 の fulfilledCallbacks に 登録される。
return result + "!!!";
});
const p4 = p3.then((result) => {
//この関数が p3 の fulfilledCallbacks に 登録される。
console.log(result);
});
// Success!!!!!! と表示される。
残念ながら、コンソール上では、promise の fufilledCallbacks に何が入っているかは見れないけど😢 このコードをコンソールにコピペして実行すると、各 promise に次の処理が登録された結果、ちゃんとそれぞれ result の値が変化しているのが分かる。
ちなみに、p4 の result が undefined なのは、p3.then() の中の処理がconsole.log()だけでresultをreturnしていないから。
さて、普通私たちが見るコード例は、こんな風にぶつぶつ区切ってなくて、new Promise().then().then()となってる。
今の例は各 promise を見せるためにぶつぶつ切っただけで、普通はそんなことは必要ないので、つなげて下のように書かれる。こういうのをpromise チェーンという。
new Promise((resolve, reject) => {
// ここに非同期処理のコードがあると想定。
let success = true; //ここでは非同期処理が成功したと想定。
if (success) {
resolve("Success!");
} else {
reject(new Error("Error has occurred"));
}
})
.then((result) => {
return result + "!!";
})
.then((result) => {
return result + "!!!";
})
.then((result) => {
console.log(result);
});
9 エラーハンドリング
できた!と思ったけれど、上のコードだと、エラーが発生したときにどう処理するか書かれていない。これだと、エラーがどこかで吸収されて表面化しないまま、プログラムが停止したり、後続のステップが前のプロセスが正常に完了した前提で誤った答えが出たりする可能性がある。
エラーハンドリングが必要だが、Promiseにはかなりの部分が事前に仕込まれている。
new Promise().then().then().then()....
通常、プロミスチェーンはこのような形で連結する。このとき、new Promise()でできたpromiseをp1とし、次の.then()でできるpromiseをp2とする。
new Promise()の中では、正規の処理が失敗したとき、投げたいエラーを reject()に渡すように書きます。
それに対して、.then()では、普通、やりたい処理だけを書き、どんなエラーを投げたいかなどは書かない。なぜかというと、前のステップからエラーが投げられてきても、自分のステップの処理でエラーが発生しても、とにかくエラーがあったら、自動的に、書かれた正規の処理はせず、エラーを次のステップにそのまま伝えるように仕組まれているから。
なので、p1で投げられたエラーはp2, p3, p4と次のステップに送られていく。もちろん、p1が上手くいっても、p2やp3でエラーが発生したら、それがPromiseチェーンの最後まで伝えられる。
そして、チェーンの最後まで伝えられたエラーをどう処理するか?それには.catch()を使う。.catch()は前のステップからエラーが伝えられてきたら、それを受け取るように仕組まれているので、それを受けてどうしたいかを書けばいい。
こうやって、Promiseチェーンはこんな完成形になる。
new Promise().then().then().then().catch()
コードのイメージ図
new Promise((resolve, reject) => {
/* 処理 A */
let success = true; //または処理が失敗したらfalseになると想定。
if (success) {
resolve("Success!");
} else {
reject(new Error("Error has occurred"));
}
})
.then((result) => { /* 処理 B */ }) //Aからエラーが来たら、処理Bはせず、エラーを次に伝える。
.then((result) => { /* 処理 C */ }) //Bからエラーが来たら、処理Cはせず、エラーを次に伝える。
.catch((error) => { //Cからエラーが来たら、受け取って処理。
console.error(error);
});
コールバックでは、各ステップごとにエラーハンドリングを書かないといけなかったのを思うとずいぶん楽になった!😀
どうしても特殊なエラーハンドリングを書きたい場合は、.then()には第2引数としてエラーハンドラを明示的に書くことができる。こうすると、書いたエラーハンドラが前のpromiseのrejectedCallbacks配列に登録され、前のpromiseで処理が失敗してそのエラーが投げられると、そのエラーハンドラで処理されるようになる。
ただし、エラーハンドラを書く際、前のステップから受け取ったエラーを、(error)=>{throw error} のような形で、次のステップに投げるのを忘れると、後続のステップに本来伝わるべきエラーが伝達されず、Promiseの仕組みは前のステップが正常に完了したと誤判断してしまうので注意が必要。
10 振り返り
いかがでしょうか。
今、上のコード例を見て、ひとつひとつの仕組みが全部意味が分かって、すっきりしていたら幸いです。
この記事では以下のような内容を取り上げました。
- PromiseはJavaScriptエンジンに実装された仕組み。
- promiseオブジェクトのプロパティとそれぞれの役割。
- resolveは処理が完了したときに走るべきアクションを実行させるように実装されている。
- rejectは処理が失敗したときのアクションを実行させるよう実装されている。
- .then()と.catch()もPromiseの仕組みの一部。それぞれがどんな機能を担っているのか…などなど。
Promiseという機能拡張によって「状態の変化があったときに次の処理を行う」という「状態管理」を、コールバックを入れ子にしなくても、JavaScriptエンジンが全部引き受けてくれることになりましたし、エラーハンドリングも、各段階で書きこまなくても、どの段階でエラーが発生しようが、次へ次へと後に伝え、最後にキャッチして処理する仕組みがJavaScriptエンジンで実装されました。
11 fetch, axios, async/await
Promise は本当に有難い機能拡張だったのですが、リクエストをURLに投げて何かをとってくるとき、今は、直接 new Promise() を書くのではなく、fetch や axios を使うのが主流です。また、非同期処理全般に、Promiseチェーンの代わりとして、async/await を使ってさらに読みやすくなっています。ただ、それだけ背後の働きは隠ぺいされています。
この記事では解説しませんでしたが、Promise の正体が分かったことで、これらもぐっと分かりやすくなるのではと期待しています。




