awaitが機能しない
とある状況でAPIを複数回叩く必要があるとして、
const delay = () => {
return new Promise(resolve => setTimeout(resolve, Math.random() * 1500))
}
// Fake API
const API = (ep) => {
return delay().then(() => console.log(`API [${ep}] is called.`))
}
それをこのようにforEachで順番に処理していきます。
const asyncInForEach = () => {
console.log('start')
const endpoints = ['endpoint1', 'endpoint2', 'endpoint3', 'endpoint4']
endpoints.forEach(async (ep) => {
await API(ep)
})
console.log('end')
}
asyncInForEach()
おそらく次のような出力になる、
start
API [1] is called.
API [2] is called.
API [3] is called.
API [4] is called.
end
と思いきや実際は違います。
APIの結果を待たずにforEach()が終了してしまいました。
これはforEach()がasync/awaitをサポートしていないためで、forEach()の中でawaitを使っても正しく動作しません。
解決策
1. for loopを使う
const asyncInForLoop = async () => {
console.log('start')
const endpoints = ['endpoint1', 'endpoint2', 'endpoint3', 'endpoint4']
for (let i = 0; i < 4; i++) {
await API(endpoints[i])
}
console.log('end')
}
asyncInForLoop()
for loopはasync/awaitをサポートしているため、問題なくawaitが機能します。
シンプルでわかりやすいですが、次のPromise.all()はより効率的に処理することができます。
2. Promise.all()を使う
Promise.all()は配列のような反復処理可能(iterable)なオブジェクトを引数にとり、
- 引数の中の全てのPromiseが解決された
- 引数の中のPromiseが一つでもrejectされた
以上2つのどちらかの条件が満たされた時点で終了します。
これを使うことで並列にAPIを処理することができます。
const asyncWithPromiseAll = async () => {
console.log('start')
const endpoints = ['endpoint1', 'endpoint2', 'endpoint3', 'endpoint4']
await Promise.all(
endpoints.map((ep) => {
return API(ep)
})
)
console.log('end')
}
asyncWithPromiseAll()
出力結果を見ると、実行された順番に関係なく出力されており、ちゃんと並列処理されているのが確認できます。
Promise.all()の注意点
さきほど触れたように、Promise.all()では一つでもPromiseがrejectされると終了してしまいます。
その方が都合が良い時もありますが、知らずに使っているとバグの元です。
先ほどの例をもう少し現実的にしてみます。
APIに失敗するケースを足して、
const delay = () => {
return new Promise(resolve => setTimeout(resolve, Math.random() * 1500))
}
const API = (ep) => {
+ if (ep === 'fail') {
+ return new Promise((_, reject) => {
+ reject(new Error('error on api call'))
+ })
+ }
return delay().then(() => `return from [${ep}]`)
}
まとめて返り値を受け取ります。
const fetchParallel = async () => {
const endpoints = [
'endpoint1',
'endpoint2',
'endpoint3',
'endpoint4',
'endpoint5',
]
const result = await Promise.all(
endpoints.map(ep => API(ep))
)
.catch(error => error.message)
console.log(result)
}
fetchParallel()
実行するとしっかりとAPIの返り値がまとめて返ってきます。
// return
[
"return from [endpoint1]",
"return from [endpoint2]",
"return from [endpoint3]",
"return from [endpoint4]",
"return from [endpoint5]"
]
では一つ失敗するエンドポイントを含めてみます。
const fetchParallel = async () => {
const endpoints = [
'endpoint1',
'endpoint2',
'endpoint3',
'endpoint4',
'endpoint5',
'fail', // added this
]
const result = await Promise.all(
endpoints.map(ep => API(ep))
)
.catch(error => error.message)
console.log(result)
}
fetchParallel()
// return
"error on api call"
ひとつのAPIがrejectされただけで、他のAPIの返り値も得られなくなりました。
これは一つ一つのPromiseにcatchを追加することで防ぐことができます。
const fetchParallel = async () => {
const endpoints = [
'endpoint1',
'endpoint2',
'endpoint3',
'endpoint4',
'endpoint5',
'fail', // added this
]
const errorHandler = (e) => {
// do something
return null
}
const result = await Promise.all(
endpoints.map(ep => API(ep).catch(errorHandler))
)
.catch(error => error.message)
console.log(result)
}
fetchParallel()
*Promise一つ一つに対するcatchとPromise.all()に対するcatchの違いに注意。
// return
[
"return from [endpoint1]",
"return from [endpoint2]",
"return from [endpoint3]",
"return from [endpoint4]",
"return from [endpoint5]",
]
これで無事に並列処理でAPIから値を取得することができました。