🚋

【JavaScript】Promise(再)入門〜クイズを通してメンタルモデルを(再)構築〜

2022/11/27に公開約5,800字2件のコメント

はじめに

フロントエンドの開発において、HTTPリクエストなどを行うときに非同期処理を書きますが、
Promiseについて自分の言葉で説明できるほど理解していない人が多いように感じます。(私もそうでした)

今回はPromiseに関するクイズを5つ出題します。
頭の中で実行結果をイメージし、解答してください。
ReactやVueなどを書いているフロントエンドの開発者や、今後開発をしていきたいと志している方には
必ず理解していて欲しい内容です。

間違えてしまった問題や自信を持って答えられなかった問題に関しては
正しい挙動を理解し、Promiseによる非同期処理のメンタルモデルを再構築していきましょう。

明日からのJSライフをより豊かなものとしていただければ幸いです。

※今回はasync/awaitについては扱いません。
※関数は基本的に全てアロー関数で書いています。

Promiseの基本の復習

まずはPromiseの基本の書き方について軽くおさらいをします。

new Promise((resolve, reject) => {
  // 同期処理
  // 必ずresolve() or reject()を実行する
}).then(() => {
  // 非同期処理
  // 前の処理でresolve()が呼ばれたことで呼ばれる関数
}).catch(() => {
  // 非同期処理
  // 前の処理でreject()が呼ばれたことで呼ばれる関数
}).finally(()=> {
  // 非同期処理
  // 必ず呼ばれる
})

Promiseインスタンスには状態があります。以下の3つです。

  • 待機 (pending): 初期状態。成功も失敗もしていません。
  • 履行 (fulfilled): 処理が成功して完了したことを意味します。
  • 拒否 (rejected): 処理が失敗したことを意味します。
    Promiseインスタンスの状態は1度fulfilledまたはrejectedに変わった場合
    2度と変えることはできません。

Promiseインスタンスの状態を意識し先ほど紹介した基本の書き方を見て処理をイメージしましょう。

new Promise((resolve, reject) => {
  // 初期状態はpending
  // 必ずresolve() or reject()を実行する
  // resolve()は状態をfulfilledに変更する
  //   thenでチェーンされている場合に、thenの第1引数の関数を非同期で呼ぶ
  // reject()は状態をrejectedに変更する
  //   catchでチェーンされている場合に、catchの引数の関数を非同期で呼ぶ
}).then(() => {
  // 明示しなくても必ずPromiseインスタンスを返す。
  // 明示しない場合は、状態がfulfilledなPromiseインスタンスを返す
  // 状態をrejectedに変更したい場合はPromise.reject()を実行する
}).catch(() => {
     // 明示しなくても必ずPromiseインスタンスを返す。
  // 明示しない場合は、状態がfulfilledなPromiseインスタンスを返す
  // 状態をrejectedに変更したい場合は return Promise.reject()を実行する
}).finally(()=> {
     // 明示しなくても必ずPromiseインスタンスを返す。
  // 明示しない場合は、状態がfulfilledなPromiseインスタンスを返す
  // 状態をrejectedに変更したい場合はreturn Promise.reject()を実行する
})

説明を見て、「あーそうだったな」でも「え、どういうこと?」となっていても大丈夫です。
実際のコードを見ながら理解を深めていきましょう。
実行結果を確かめたい場合はブラウザのコンソールや、JS Fiddleなどの実行環境でお試しください。

Promiseクイズ(計5問)

レベル1(基礎編)(3問)

Q1 実行結果をイメージしてください。何が出力されるでしょうか。(Promise初期化)

new Promise((resolve, reject) => {
}).then(() => {
	console.log("called then")
}).catch(() => {
	console.log("called catch")
}).finally(()=> {
	console.log("called finally")
})
Q1 答え

何も出力されません。new Promiseでインスタンスを作成したときの状態はpendingです。
pendingのままではチェーンしているメソッドは呼び出されません。

new Promise((resolve, reject) => {
	resolve() // 状態がfulfilledとなりthenの第1引数のメソッドを非同期で呼ぶ
}).then(() => {
	console.log("called then")
}).catch(() => {
	console.log("called catch")
}).finally(()=> {
	console.log("called finally")
})
// "called then"
// "called finally"

Q2 実行結果をイメージしてください。何が出力されるでしょうか。(複数catch)

new Promise((resolve, reject) => {
	reject()
}).then(() => {
	console.log("called then")
}).catch(() => {
	console.log("called catch 1")
}).catch(()=> {
	console.log("called catch 2")
}).finally(()=> {
	console.log("called finally")
})
Q2 答え

以下が出力されます。
"called catch 1"
"called finally"
1つ目のcatchの関数が実行されるところは問題ないと思います。
thenやcatchの関数の実行時の戻り値は、明示していない場合、状態がfulfilledなPromiseインスタンスを返します。
そのため、2つ目のcatchにチェーンするのは状態が1つ目のcatchの関数の戻り値がrejectedとなったPromiseインスタンスを返す場合のみです。
以下のようにcatch内の関数でreturn Promise.rejected()とし、
状態がrejectedなPromiseインスタンスを返した場合にcatchで再度チェーンすることができます。

new Promise((resolve, reject) => {
	reject()
}).then(() => {
	console.log("called then")
}).catch(() => {
	console.log("called catch 1")
	return Promise.reject()
}).catch(()=> {
	console.log("called catch 2")
}).finally(()=> {
	console.log("called finally")
})
// "called catch 1"
// "called catch 2"
// "called finally"

Q3 実行結果をイメージしてください。何が出力されるでしょうか。(then/catchチェーン)

new Promise((resolve, reject) => {
	resolve()
}).then(() => {
	console.log("called then 1")
	return Promise.reject()
}).catch(() => {
	console.log("called catch")
	return
}).then(()=> {
	console.log("called then 2")
})
Q3 答え

以下が出力されます。
"called then 1"
"called catch"
"called then 2"

catch内でreturnをしていますが、returnは書いても書かなくても、必ずfulfilledなPromiseインスタンスを返します。
catchでもthenでもPromise.reject()などでrejectedなPromiseインスタンスをreturnしない限り、thenにチェーンします。

new Promise((resolve, reject) => {
	resolve()
}).then(() => {
	console.log("called then 1")
	return Promise.reject()
}).catch(() => {
	console.log("called catch")
	return
}).then(()=> {
	console.log("called then 2")
})
// called then 1
// called catch
// called then 2

レベル2(中級編)(2問)

Q4 実行結果をイメージしてください。何が出力されるでしょうか。(引数のtype)

new Promise((resolve, reject) => {
	resolve()
}).then((val) => {
	console.log(typeof(val))
}).then((val)=> {
	console.log(typeof(val))
})
Q4 答え

以下が出力されます。
undefined
undefined
resolve()時に引数に値を渡した場合はチェーン先のメソッドその値を受け取ることができますが、今回は何も渡していないためundefinedとなります。
同様にthen内でも戻り値を何も明示しない場合はチェーン先のメソッドには何も渡さないためundefinedとなります。
thenやcatchでチェーン先に値を渡すにはreturnで戻り値を設定します。

new Promise((resolve, reject) => {
	resolve("resolve!")
}).then((val) => {
	console.log(typeof(val)) // string
	return 1
}).then((val)=> {
	console.log(typeof(val)) // number
})

Q5 実行結果をイメージしてください。何が出力されるでしょうか。(エラー処理)

Promise.resolve("success!")
  .then(() => {
	  throw new Error("error!")
  }).catch((e) => {
	  return "success!"
  }).then((val) => {
	  throw Error("error!")
  }).catch((e)=> {
	  console.log(e.message)
  })
Q5 答え

以下が呼ばれます。
"error!"
1行目のPromise.resolve("success!")ですが以下と全く同じ意味です。

new Promise((resolve, reject) => {
	resoleve("success!")
})

catchにチェーンするにはrejectedなPromiseインスタンスが返る場合と話しましたが、
それ以外に、エラーがthrowされた場合にもcatchにチェーンします。
今回チェーンはthen.catch.then.catchとなっていますが、全てのthenとcatchが実行され、
最終的にはconsole.log(e.message)が実行されます。

終わりに(おすすめ学習サイト紹介)

いかがだったでしょうか?
自信を持って回答できていればPromiseの基礎についてはバッチリだと思います。
今後もう少し問題を増やしていく予定です(反響があればですが)
Promiseについて少しでも自信を持って明日からコードを読み書きできるようになれば幸いです。

Promiseの勉強のおすすめサイトをご紹介します。
https://jsprimer.net/basic/async/#promise

https://azu.github.io/promises-book/

Discussion

Q2 の答えは typo になっていないでしょうか?

答えの内容に触れるので念のため折りたたみます。

コメント

以下が出力されます。
"called catch 1"
"called finally"

実際に動かすと "called finally" は表示されません。

$ cat q2.js
new Promise((resolve, reject) => {
        reject()
}).then(() => {
        console.log("called then")
}).catch(() => {
        console.log("called catch 1")
}).catch(()=> {
        console.log("called catch 2")
})

$ node q2.js
called catch 1

解説からするとコード側で finally() が抜けているわけではなさそうですが、答えの方にあわせて finally が表示されるようにするとチェーンのイメージが掴みやすいのかなとも思いました。

大変失礼いたしましたm(__)m
ご指摘いただいたように修正いたしました。
大変助かります。ありがとうございます!

ログインするとコメントできます