【Javascript】配列要素をループする際のasyncとawaitで困った話


タイトルの通り。
正直に言うとasyncやawaitはどうにも知識が表面的で、なんとなくでやってる部分があったのだが、というか今もあるので実際こうして困った事例が出てきたわけだが、とにかく未来の自分が同じ問題に遭遇する場合に備えてまとめておく。


実例

簡単に実例を紹介する。
初見の段階だと自分から見ればどう見てもasync function内でawaitを使っているはずだったので(違ったのだが)「awaitはasync function内で使え」と文句言われる理由が全く持って理解できなかったのだ。

例えばこういう関数があるとする

module.exports = async (arg) => {  
    console.log(`this is async function,your argument is ${arg}`)  
}  

これを呼び出す別モジュールを以下のように記述する。

const af = require('./async_func.js')  
var arr = ['aaa','bbb','ccc']  
(async()=>{  
        arr.forEach((e)=>{  
            await af(e)  
        })     
})()  

なんのことはない、配列をforEachで回して、取り出した要素を一つずつ↑のasync functionに渡してるだけだ。
呼び出してるaf()はasync functionなのでawaitを付けて同期的に呼び出すように指示している。

でもこれはダメなのである。
実際実行するとエラーになる↓

await af(e)  
^^^^^  
  
SyntaxError: await is only valid in async function

しかし匿名関数とはいえ、af()全体をasync()のブロックで囲っているのだから、「async function内」であることは間違いないのでは?と思っていた。
async()ブロックで囲ってるのが悪いのかな、async function定義つくってそっちにするか、と思って

const af = require('./async_func.js')  
var arr = ['aaa','bbb','ccc']  
async function callAsyncFunction() {  
        arr.forEach((e)=>{  
            await af(e)  
        })  
}  
(async()=>{  
    await callAsyncFunction()     
})()  

としてみたが、これでもやはり同じエラーが出た。
なんだこりゃ?どうすりゃいいんだ??と思っていた。

原因

Array.forEachの引数部分が別のfunctionになっていて、それがasyncじゃなかったからだった。
Array.forEach( (e)=>{ ... } )←ここんところ。

上記で言うところの赤太字部分はArray.forEachの引数にあたるfunction定義になっていて、ここが独立したfunctionのコードブロックになるようで、「(Array.forEachの引数に渡してる関数は)async functionじゃないよ」という意味でエラーになっていたようだ。
まあそういわれてみれば確かにそりゃその通り。。。

このコードブロックは、上記のように、「Array.forEachを大きくasyncブロックで囲っている」というのとは無関係に優先される。
たとえ処理の全体がasyncになっていようとも、forEachの中だけは治外法権で、functionを書かなければいけなくなるのだ。

なので無理やりにでも以下のような記述にするととりえあえずこのエラーは回避できる。

...  
arr.forEach(async (e)=>{  
    await af(e)  
})  
...

forEachの引数のfunction定義を書く前にasync句をつけてしまうのである。
個人的には見慣れないコードなので一瞬「ん…?」と思ってしまうが別に間違いじゃない。
ちゃんと動作もする

this is async function,your argument is aaa  
this is async function,your argument is bbb  
this is async function,your argument is ccc

ただまあこう書くくらいなら素直に普通のfor文書いた方が読みやすい気はする。

(async()  
    for (e of arr) {  
        await af(e)  
    }  
)()  

どうもfor文のブロックはforEachと違って「関数」とはみなされないようで(まあそりゃそうか)、上記のような記述をすれば、全体をasyncブロックでかこうだけで、forEachのときに比べると引数のfunctionにasyncつけるとか小細工しなくても動作する。
個人的にはこっちのほうがわかりやすかった。

まとめ

要するにArray.forEachの処理内でasync functionを呼び出そうとするときには注意が必要だね、ということだったようだ。
一般常識かもしれないけど知ら鳴ったのでメモとして残す。