I discovered a weird issue while trying to implement a function that awaits all promises of an array and catches all errors. This implementation led to a complete application crash caused by an unhandled promise rejection.
const completeAllPromises = async (promises) => {
const errors = []
for (let i = 0; i < promises.length; i++) {
await promises[i].catch((error) => {
error.message = `${i}: ${error.message}`
errors.push(error)
})
}
if (errors.length) return Promise.reject(errors)
}
const timeout = (duration) => {
return new Promise((resolve) => setTimeout(() => resolve()), duration)
}
const fail = async () => {
return Promise.reject(new Error('Failed'))
}
const crash = async () => {
await completeAllPromises([timeout(100), fail()]).catch(
(error) => console.log(`Crash prevented, catch promise rejection`, error), // never be called
)
}
const main = async () => {
// will crash and catch will not be called
await crash().catch((error) => console.log('Finished crash() and catch error', error))
}
main()
It seems like the catch implementation in completeAllPromises
function is executed at runtime, so while awaiting the first timeout promise, fail()
will reject and not be catch. The weird thing for me is that the crash()
and also not main()
will catch this error. It almost seems like the call chain is not resolvable at the time fail()
rejects.
Switching timeout(100)
and fail()
will result in a properly catch errors. Using a try and catch block will also lead to the same issue.
Solution ✅
const completeAllPromises = async (promises) => {
const errors = []
const results = await Promise.allSettled(promises)
results.forEach((result) => {
if (result.status === 'rejected') errors.push(result.reason)
})
if (errors.length) return Promise.reject(errors)
}
const timeout = (duration) => {
return new Promise((resolve) => setTimeout(() => resolve()), duration)
}
const fail = async () => {
return Promise.reject(new Error('Failed'))
}
const crash = async () => {
await completeAllPromises([timeout(100), fail()]).catch(
(error) => console.log(`Crash prevented, catch promise rejection`, error), // never be called
)
}
const main = async () => {
// will crash and catch will not be called
await crash().catch((error) => console.log('Finished crash() and catch error', error))
}
main()