0
1
はじめての記事投稿
Qiita Engineer Festa20242024年7月17日まで開催中!

awaitの挙動からsetTimeoutがawaitされない理由を紐解く

Posted at

はじめに

async/awaitを使っていて制御の順番が思い通りにならなくて困ったことは誰しもあると思います。
そんな方のために、awaitの挙動からなぜ書いたコードが思い通りの順番で実行されないのか少しだけ深掘りしていきます。
今回はsetTimeout()編です。

目次

  1. async/awaitとは
  2. 待ってくれないawait
  3. awaitはPromiseインスタンスの値を取り出す
  4. setTimeout()はPromiseじゃない
  5. awaitにPromise以外を待たせようとしても待ってくれない
  6. それでも待たせたいなら

async/awaitとは

async/awaitとは、非同期処理を行う際に処理の順番をコントロールするために使う記述です。
例えば以下のようにasync関数内でfetch()を行う関数を実行すると、コード上の順番と処理の実行順がずれます。

asyncFunc.js
const asyncFunction = async(url) => {
    const data = await fetch(url).data;
    console.log(data);
}

console.log("start");
asyncFunction(取得したいURL);
console.log("finish");
出力結果
start
finish
(取得したデータ)

取得したデータのコンソールへの出力が最後になりましたね。

待ってくれないawait

このような例から、await式は「非同期処理の完了を待つ」記述だといわれます。
しかし本質的には、「Promiseインスタンスの値を取り出す」という操作が行われています。
これをきちんと理解しないと、「非同期処理なのにawaitが待ってくれない」という残念な現象が発生します。

thisIsBad.js
const asyncFunction = async() => {
    await setTimeout(() => console.log("Please wait"), 1000)
    console.log("Cannot wait");
}

console.log("start");
asyncFunction();
console.log("finish");
出力結果
start
finish
Cannot wait
---
(1秒後)
Please wait

確かに"start"→asyncFunction→"finish"という順番がstart→finish→asyncFunctionという順番になりましたが、"Cannot wait"が"Please wait"を待たずして出力されているため、肝心のasyncFunction内部ではawaitが機能していないようです。
なぜなのか順に説明していきます。

awaitはPromiseインスタンスの値を取り出す

awaitの機能をもう少し正確に記述すると、それは 「右にあるPromiseの確定した値を評価する(取り出す)」 という機能になります。
ここで一つ押さえておきたいのがPromiseについてです。
Promiseインスタンスは3つの State(状態) を持ち、それぞれ

  • Pending(待機状態)
  • Fulfilled(履行状態)
  • Rejected(拒否状態)

があります。Pendingはその名の通り、まだ実行されていない状態ですが、FulfilledとRejectedは値が確定し、これ以上変わらなくなります。
例えばそれは、fetch()で正しくデータを取得できた、あるいはできずに例外が発生した時だとか、Promise.resolve()Promise.reject()されたときなどです。
awaitはPromiseインスタンスがFulfilledやRejectedになって確定した値を評価しよう(取り出そう)と「待つ」わけです。

setTimeout()はPromiseじゃない

実は、setTimeout()は確かに非同期処理を行うことができますが、Promiseではないのです。

setTimeout.js
console.log(setTimeout(() => console.log("a"), 1000));
出力結果
15599(この値は毎回異なります)
---
(1秒後)
a

そうです。setTimeout()即座にタイマーIDを返しますから、これ自体Promiseではありません。

awaitにPromise以外を待たせようとしても待ってくれない

Promiseの確定した値を取り出すために、PromiseインスタンスがFulfilledやRejectedになるのを待つawaitですが、Promiseではなく、ほかの値を渡すとどうなるでしょうか。
それが、先ほどの例になります。

thisIsBad.js
const asyncFunction = async() => {
    await setTimeout(() => console.log("Please wait"), 1000)
    console.log("Cannot wait");
}

console.log("start");
asyncFunction();
console.log("finish");
出力結果
start
finish
Cannot wait
---
(1秒後)
Please wait

そう、awaitは右にある値がPromiseインスタンスではない場合、即座に値を取り出してしまいます。
そして、setTimeout()は実行されると即座にタイマーIDを返すんでしたよね。
よって、setTimeout()をawaitすると、awaitはタイマーIDをsetTimeout()からの値だと判断して評価を終了し、もう終わった処理扱いしてしまうのです。
そして、thisISBad.jsのようにほかの処理がない場合、await以降の処理の開始がsetTimeout()のタイマーの終了よりも早くなってしまうのです。

それでも待たせたいなら

setTimeout()をawaitで待ってから処理を実行したいケースというのはあまり考えられませんが、それでもPromise以外のものをawaitで待ちたいことはあるかもしれません。
そこで用いられるのが、Promisificationという方法です。
一言でいえば、Promiseインスタンスでラップしてしまうということです。

Promisification.js
const promisifiedTimer = (delay) => {
    return new Promise((resolve) => {
        setTimeout(() => {
          console.log("Please wait")
          resolve()
        }, delay)
    })
}

const asyncFunction = async() => {
    await promisifiedTimer(1000)
    console.log("I can wait");
}

console.log("start");
asyncFunction();
console.log("finish");

こうすれば、promisifiedTimerがPromiseインスタンスを返すようになるので、awaitがきちんとSettledになるまで待ってくれます。よって、promisifiedTimer内でやりたい操作を行ってからresolveするようにすれば、思い通りの実行順になるというわけです。

0
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1