Promiseとasync/awaitが正しく機能しない時に読む記事

May 10, 2020

これらはエラーが出ないエラーとして良く遭遇する。特に処理を「待ってくれない」パターンだ。この問題をケース別にまとめた。

promise_await_mattekurenai

Proimseasync/awaitについて再確認する

通常これらは非同期処理が終了したかどうかを判断するために利用される。

しかし書き方が少し複雑で構文を少しでも間違えていてもエラーが出ること無く意図しない動作(非同期処理を待たず次にいってしまう)をする事が頻繁にあるため整理しておく。

この記事のターゲット

Promiseおよびasync/awaitはこれまでに使ったことがある人向け。

基本的な説明はしないので注意。

この記事では「ぱっと見で正しい動作をしそうだが、なぜか動作しない」例をいくつか示す。

アローファンクションになっていないPromise

まず最初のコードは次の数字順で出力される想定で組んでいる。

  • 1: Start
  • 2: End (Promiseは処理をブロックしないのでEndが先にでるのは正常)
  • 3: setTimeout
  • 4: then

5: errorはエラー処理のため表示されない想定

console.log("1: Start")
new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log("3: setTimeout")
        resolve()
    }, 5000)
}).then(
    console.log("4: then")
).catch(
    console.log("5: error")
)
console.log("2: End")

だが、実際には次の実行結果となる。

1: Start
4: then
5: error
2: End
3: setTimeout

これは実行時エラーもなく正常に終了するが結果がおかしい。thenerrorが両方出ておりsetTimeoutは一番最後にきている(Promiseが無視されている)

原因

thenおよびcatchのコードがアローファンクションになっていないため。

次に直せば正しく動作する

console.log("1: Start")
new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log("3: setTimeout")
        resolve()
    }, 5000)
}).then(() => {
    console.log("4: then")
}).catch(() => {
    console.log("5: error")
})
console.log("2: End")

実行結果

1: Start
2: End
3: setTimeout
4: then

もう一度言うがEndが先に出るのは正常である。

resolveがないPromise

さてresolveを記述し忘れるとどんな動作になるだろうか。

先程の正しい実行結果を得られたコードからresolveを消去してみる。

※ここからは分かりやすいように各項目の数字を上から順に変更した。

console.log("1: Start")
new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log("2: setTimeout")
        //ここのresolve()を消去した
    }, 5000)
}).then(()=>{
    console.log("3: then")
}).catch(()=>{
    console.log("4: error")
})
console.log("5: End")

実行結果

1: Start
5: End
2: setTimeout

resolveがないのでthen実行されなかった。

同様に処理の失敗をハンドリングする場合はrejectも正しく書かねばならない。

awaitを書いているのに待ってくれないケース

一応書いておくとPromiseは通常thenrejectをアローファンクションで結ぶ必要があるり面倒なのでPromiseの前にawaitを置いてやるだけで同期的に処理を待つ事ができるようにするコードだ。

...と言われて次のコードを書くが処理を待たず次に行ってしまう

asyncfunctionに宣言する必要があるのでコードは関数となっている。

const awaitFunc = async () => {
    console.log("1: Start")
    await new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("2: setTimeout")
        }, 2000)
        resolve()
    })
    console.log("3: End")
}

このコードも1,2,3と順番に出ることを想定している。

しかし、実行結果は次のように処理を待たずEndが出てしまう。

1: Start
3: End
2: setTimeout

原因

文法も正しくエラー表示なしにもかかわらずawaitが機能していないように見える。なぜだろうか。

これはresolveの位置が悪い。

よくコードを見るとresolveは正しくpromise内にあるが、setTimeoutが非同期で処理を開始した直後に次のresolveコードが実行されるので先に3: endが表示される

これはawait関係なくPromiseのみの場合でも発生するので注意。

awaitは正しく機能するのにそれ以降の処理が実行されないケース

次のコードは3: Endが表示されない。

const awaitFunc = async () => {
    console.log("1: Start")
    await new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("2: setTimeout")
        }, 2000)
    })
    console.log("3: End")
}

// 結果
// 1: Start
// 2: setTimeout

原因

これは前にも述べたがresolveを書き忘れているケース。

awaitresolveが発生するまで処理をブロックするのでそれ以降のコードは実行されないままだ。

そもそもawaitを書くとエラーになるケース

const awaitFunc = () => {
    console.log("1: Start")
    await new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("2: setTimeout")
            resolve()
        }, 2000)
    })
    console.log("3: End")
}

原因

これはすぐ分かるかもしれないがawaitFunc = () の箇所にasyncが無い。

es6構文となっており次のように書く人も多いかと思うので両方記載しておく。

async function awaitFunc() { ... //これでOK
const awaitFunc = async () => { ... //es6はこれ

↑は厳密には違う2つになるらしい。詳しくは私もわからない。

async/awaitは書いているのに文法エラーが出る場合

次のエラーが出る場合。

SyntaxError: Unexpected reserved word

コード

const awaitFunc = async () => {
    console.log("1: Start")
    new Promise((resolve, reject) => {
        await setTimeout(() => {
            console.log("2: setTimeout")
            resolve()
        }, 2000)
    })
    console.log("3: End")
}

原因

これはあまり難しくない。awaitPromiseの中に入ってしまっている。

コードが長くなってくるとasync functionの中にawaitを正しく入れているのになぜ動作しないの?となるケースはよくある。

Promiseではないものをawaitしているケース

これもよくありそうだ。何度も言うがPromiseawaitする必要がある。

const awaitFunc = async () => {
    console.log("1: Start")
    await setTimeout(() => {
        console.log("2: setTimeout")
    }, 2000)
    console.log("3: End")
}

実行結果

1: Start
3: End
2: setTimeout

setTimeoutpromiseではないのでエラーなしで3: Endが先に実行されてしまう。

※ちなみにsetTimeoutTimeoutという名前のオブジェクトがreturnされる。

おまけ1:awaitを入れるとPromiseReturnされない

const awaitFunc = async () => {
    const test = await new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve()
        }, 2000)
    })
    console.log(test)
}

// 結果
// undefined

undefinedが帰るがこれは仕様のようだ。

awaitを外すと Promise { <pending> } が帰ってくる。

おまけ2:await + thenでどうなる?

正しく動作するawaitPromsiethenをつけたらどうなるか。

const awaitFunc = async () => {
    console.log("1: Start")
    await new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("2: setTimeout")
            resolve()
        }, 2000)
    }).then(() => {
        console.log("4: then")
    })
    console.log("3: End")
}

実行結果

1: Start
2: setTimeout
4: then
3: End

先に4: then、その後に3: Endが処理される。

おまけ3:Promise.allawaitできる

const awaitFunc = async () => {
    console.log("1: Start")
    const test = new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("2: setTimeout")
            resolve()
        }, 2000)
    })
    Promise.all([test]).then(()=>{
        console.log("3: Promise.all")
    })
    console.log("4: End")
}

少し複雑だが、このコードは4: Endが先に出てしまう。

1: Start
4: End
2: setTimeout
3: Promise.all

次のコードのようにPromise.allawaitすることも可能だ。

Promise.allPromiseオブジェクトをリターンするから、といえば納得が行く。

const awaitFunc = async () => {
    console.log("1: Start")
    const test = new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("2: setTimeout")
            resolve()
        }, 2000)
    })
    await Promise.all([test]).then(()=>{ //Promise.allにはPromiseの「配列」を渡すこと!
        console.log("3: Promise.all")
    })
    console.log("4: End")
}

実行結果

1: Start
2: setTimeout
3: Promise.all
4: End

※このコードは良くない。なぜならこの処理なら4: EndPromise.allの中に書けば良い。

所感

例を挙げ始めたらきりがなくまだ色々なケースがありそうだ。

実は私も「あれ?なんで動かない?」となる場面がしばしばあるため記事にまとめた。

よく見れば分かるもののコードが長くなればより発生しやすい。

読みやすいコードも意識して書いていきたいところである。

この記事をシェア:

author icon

仮想トイレ @CrypticToilet
プログラミングや仮想通貨のシステムトレードに関する情報を更新中!どんな情報を流しても詰まらないトイレ。