Edited at

何問分かる?Promise に関するクイズ13問【解説付き】


イントロダクション

JavaScript を使う上で非同期処理、特に promise を理解することは非常に重要である。async/await も promise の上に成り立っているので、promise を理解して始めて正しく使うことができる。この記事では JavaScript における非同期処理をどのくらい理解できているかをチェックするための問題を紹介する。

それぞれのコードについて、実行したときにコンソールに何がどういう順序で出力されるかを考えてみよう。なお、コードはブラウザーで実行するものとし、解答は ECMAScript SpecificationHTML Living Standard に沿ったものとなっている。


🚗 初級


問題1.1

const p1 = Promise.resolve(1)

const p2 = p1.then(x => x * 2)
console.log(p1 === p2)


解答

false

promise の then catch メソッドは呼び出すたびに新しい別の promise が作成され、それが戻り値となる。元の promise に変更を加えているわけではない(内部的には promise のイベントリスナーリスト的なものがありそれに追加しているのだが、少なくとも JavaScript のユーザーからは完全に隠蔽されている)。当たり前といえば当たり前だが、勘違いしてはいけない重要なポイントである。


問題1.2

Promise.resolve()

.then(() => { throw new Error("thrown") })
.then(() => console.log(1))
.then(() => console.log(2))
.catch(() => console.log("caught"))
.then(() => console.log(3))


解答

caught

3

promise の then コールバックで例外が発生すると、thenの戻り値の promise は失敗したとみなされる。いったん promise が失敗すると、promise チェーン上をたどって catch コールバック (あるいは then の第2引数)に至るまで、then コールバックは無視される。

catch コールバックが成功すれば、catch の戻り値の promise は成功したとみなされ、最後の then コールバックも呼び出されることになる。


問題1.3

Promise.resolve().then(() => {

return Promise.resolve(Promise.resolve(Promise.resolve(1)))
}).then(console.log)


解答

1

promise の promise の promise...と何重にラップしても、then コールバックに渡されるのはアンラップされた、非 promise の値である。(この意味で then メソッドは promise の flatMap だと見ることもできる。)


問題1.4

Promise.resolve(1).then(

() => { throw new Error("oops") },
() => console.log(2)
).catch(() => console.log(3))


解答

3

p.then(f1, f2)f1 が失敗しても f2 は実行されない。p が失敗したときのみ実行される。反対に、p.then(f1).catch(f2)f1 が失敗すると f2 が実行される。


🚄 中級


問題2.1

Promise.resolve().then(() => console.log(1))

console.log(2)


解答

2

1

promise のコールバックは、必ず現在実行されている同期処理が終了してから(=コールスタックが空になってから)実行される。console.log(2) が実行されてからコールスタックが空になるため、1 が後に出力される。


問題2.2

setTimeout(() => console.log(1), 0)

console.log(2)


解答

2

1

前の問題と同じく、setTimeout のコールバックはコールスタックが空になってから実行される。


問題2.3

const p1 = new Promise((resolve) => {

console.log(1)
resolve()
})
console.log(2)


解答

1

2

promise のコンストラクターに渡した関数は、同期的にすぐに実行される。ただし、同期的に resolve してもコールバックは同期的に呼び出されない。


問題2.4

Promise.resolve()

.then(() => console.log(1))
.then(() => console.log(2))

Promise.resolve()
.then(() => console.log(3))
.then(() => console.log(4))


解答

1

3
2
4

promise が成功するとそのコールバックは microtask queue というキュー(待ち行列)に入れられる。このコードが最後まで実行されると、() => console.log(1)() => console.log(3) の順にキューに入った状態になる。このキューはFIFO(最初に入れられたものから最初に処理される)であるため、() => console.log(1) がまず処理され、() => console.log(2) がキューに入れられ、() => console.log(3) が処理され、() => console.log(4) がキューに入れられ...という具合に進む。


問題2.5

!(async () => {

console.log(1)
const x = await 1
console.log(2)
})()
console.log(3)


解答

1

3
2

async function 内のコードは、await 式が評価されるまでは同期的に実行される。そのため、1 が先に出力される。await する値がたとえ promise でなくとも、それ以降の処理は Promise.resolve().then(() => { ... }) と同様に、コールスタックが空になってから(つまり非同期に)実行される。


問題2.6

!(async () => {

let x = 0
const p1 = Promise.resolve().then(() => {
x = 1
return 10
})
console.log(x + (await p1) * x)
})()


解答

10

これは式 x + (await p1) * x がどのような順序で評価されるかを考慮する必要がある。といっても、この場合は左から計算していくだけなのでシンプルである。


  1. 左の x が評価される。この時点では 0なので 0 が返る。


  2. await p1 が評価される。x1 になり、10 が返る。

  3. 右の x が評価され、1 が返る。


  4. (await p1) * x が評価され、10 * 1 = 10 が返る。


  5. x + (await p1) * x が評価され、0 + 10 = 10 が返る。


🚀 上級


問題3.1

setTimeout(() => console.log(1), 0)

Promise.resolve().then(() => console.log(2))


解答

2

1

実行予定の promise のコールバックと MutationObserver のコールバックは microtask queue というキューに入れられる。一方でそれ以外の非同期なコールバックは task queue と呼ばれるものに入れられる。コールスタックが空になるとまず microtask queue から実行される。それが空になるとようやく task queue から1つだけ task が取り出され、また microtask queue が実行され...と繰り返される。

この問題では () => console.log(2) が microtask、() => console.log(1) が task となる。


問題3.2

setTimeout(() => {  // callback1

console.log(1)
Promise.resolve().then(() => console.log(2))
}, 0)

setTimeout(() => { // callback2
console.log(3)
Promise.resolve().then(() => console.log(4))
}, 0)

Promise.resolve()
.then(() => console.log(5))
.then(() => console.log(6))


解答

5

6
1
2
3
4

1つ前の問題と同様に考えればよい。ただし、microtask queue は空になるまで実行されるということと、task queue は1つづつしか実行されないということに注意する必要がある。


  1. callback1 と callback2 が task queue に入れられ、() => console.log(5) が microtask queue に入れられる。


  2. () => console.log(5) が実行され、() => console.log(6) が microtask queue に入れられる。


  3. () => console.log(6) が実行され、microtask queue が空になる。

  4. callback1 が実行され、() => console.log(2) が microtask queue に入れられる。


  5. () => console.log(2) が実行され、microtask queue が空になる。

  6. callback2 が実行され、() => console.log(4) が microtask queue に入れられる。


  7. () => console.log(4) が実行される。


問題3.3

<script>  // script1

console.log(1)
setTimeout(() => console.log(2), 0)
Promise.resolve().then(() => console.log(3))
</script>
<script> // script2
console.log(4)
setTimeout(() => console.log(5), 0)
Promise.resolve().then(() => console.log(6))
</script>


解答

1

3
4
6
2
5

この問題のポイントは、script タグが実行し終わって終了タグまで来るとコールスタックが空になり microtask queue が実行される1ということである。そのため、script タグと script タグの間で microtask queue が実行される。しかし、ページ全体のHTMLをパージングし終わるまでが1つの task なので、それまで次の task は実行されない。この点に注意して前の問題と同様に考えれば良い。


  1. script1 が実行され、() => console.log(2) が task queue に、() => console.log(3) が microtask queue に入れられる。

  2. コールスタックが空になり、microtask () => console.log(3) が実行される。

  3. script2 が実行され、() => console.log(5) が task queue に、() => console.log(6) が microtask queue に入れられる。

  4. コールスタックが空になり、microtask () => console.log(6) が実行される。

  5. HTML全体をパースするタスクが終了し、task () => console.log(2) が実行される。

  6. 5 の task が終了し、microtask queue が空なので task () => console.log(5) が実行される。


最後に

JavaScript を普段使いするためには少なくとも初級の問題は理解しておいたほうがよいだろう。microtask と task、コールスタックについては筆者の別記事でより分かりやすく説明している。

注意すべき点として、Node.js はイベントループの仕組みがブラウザのそれとは異なり、ここであげたコードを実行すると違う結果になることがある。また、v11 で非同期処理の挙動が変わっているようなので、使用するバージョンに気をつけたほうがいいだろう。


仕様の該当箇所





  1. script タグが実行し終わると clean up after running script が実行されてその中で microtask checkpoint というものが実行され、microtask queue 内の microtask が実行される。