Functions should be simple and do just one thing. I would start with a generic sleep -
const sleep = ms =>
new Promise(r => setTimeout(r, ms))
Using simple functions we can build more sophisticated ones, like timeout -
const timeout = (p, ms) =>
Promise.race([ p, sleep(ms).then(_ => { throw Error("timeout") }) ])
Now let's say we have a task, myTask that takes up to 4 seconds to run. It returns successfully if it generates an odd number. Otherwise it rejects, "X is not odd" -
async function myTask () {
await sleep(Math.random() * 4000)
const x = Math.floor(Math.random() * 100)
if (x % 2 == 0) throw Error(`${x} is not odd`)
return x
}
Now let's say we want to run myTask with a timeout of two (2) seconds and retry a maximum of three (3) times -
retry(_ => timeout(myTask(), 2000), 3)
.then(console.log, console.error)
Error: 48 is not odd (retry 1/3)
Error: timeout (retry 2/3)
79
It's possible myTask could produce an odd number on the first attempt. Or it's possible that it could exhaust all attempts before emitting a final error -
Error: timeout (retry 1/3)
Error: timeout (retry 2/3)
Error: 34 is not odd (retry 3/3)
Error: timeout
Error: failed after 3 retries
Now we implement retry. We can use a simple for loop -
async function retry (f, count = 5, ms = 1000) {
for (let attempt = 1; attempt <= count; attempt++) {
try {
return await f()
}
catch (err) {
if (attempt <= count) {
console.error(err.message, `(retry ${attempt}/${count})`)
await sleep(ms)
}
else {
console.error(err.message)
}
}
}
throw Error(`failed after ${count} retries`)
}
Now that we see how retry works, let's write a more complex example that retries multiple tasks -
async function pick3 () {
const a = await retry(_ => timeout(myTask(), 3000))
console.log("first pick:", a)
const b = await retry(_ => timeout(myTask(), 3000))
console.log("second pick:", b)
const c = await retry(_ => timeout(myTask(), 3000))
console.log("third pick:", c)
return [a, b, c]
}
pick3()
.then(JSON.stringify)
.then(console.log, console.error)
Error: timeout (retry 1/5)
Error: timeout (retry 2/5)
first pick: 37
Error: 16 is not odd (retry 1/5)
second pick: 13
Error: 60 is not odd (retry 1/5)
Error: timeout (retry 2/5)
third pick: 15
[37,13,15]
Expand the snippet below to verify the result in your browser -
const sleep = ms =>
new Promise(r => setTimeout(r, ms))
const timeout = (p, ms) =>
Promise.race([ p, sleep(ms).then(_ => { throw Error("timeout") }) ])
async function retry (f, count = 5, ms = 1000) {
for (let attempt = 0; attempt <= count; attempt++) {
try {
return await f()
}
catch (err) {
if (attempt < count) {
console.error(err.message, `(retry ${attempt + 1}/${count})`)
await sleep(ms)
}
else {
console.error(err.message)
}
}
}
throw Error(`failed after ${count} retries`)
}
async function myTask () {
await sleep(Math.random() * 4000)
const x = Math.floor(Math.random() * 100)
if (x % 2 == 0) throw Error(`${x} is not odd`)
return x
}
async function pick3 () {
const a = await retry(_ => timeout(myTask(), 3000))
console.log("first", a)
const b = await retry(_ => timeout(myTask(), 3000))
console.log("second", b)
const c = await retry(_ => timeout(myTask(), 3000))
console.log("third", c)
return [a, b, c]
}
pick3()
.then(JSON.stringify)
.then(console.log, console.error)
And because timeout is decoupled from retry, we can achieve different program semantics. By contrast, the following example not timeout individual tasks but will retry if myTask returns an even number -
async function pick3 () {
const a = await retry(myTask)
const b = await retry(myTask)
const c = await retry(myTask)
return [a, b, c]
}
And we could now say timeout pick3 if it takes longer than ten (10) seconds, and retry the entire pick if it does -
retry(_ => timeout(pick3(), 10000))
.then(JSON.stringify)
.then(console.log, console.error)
This ability to combine simple functions in a variety of ways is what makes them more powerful than one big complex function that tries to do everything on its own.
Of course this means we can apply retry directly to the example code in your question -
async function main () {
await retry(callA, ...)
await retry(callB, ...)
await retry(callC, ...)
return "done"
}
main().then(console.log, console.error)
You can either apply timeout to the individual calls -
async function main () {
await retry(_ => timeout(callA(), 3000), ...)
await retry(_ => timeout(callB(), 3000), ...)
await retry(_ => timeout(callC(), 3000), ...)
return "done"
}
main().then(console.log, console.error)
Or apply timeout to each retry -
async function main () {
await timeout(retry(callA, ...), 10000)
await timeout(retry(callB, ...), 10000)
await timeout(retry(callC, ...), 10000)
return "done"
}
main().then(console.log, console.error)
Or maybe apply timeout to the entire process -
async function main () {
await retry(callA, ...)
await retry(callB, ...)
await retry(callC, ...)
return "done"
}
timeout(main(), 30000).then(console.log, console.error)
Or any other combination that matches your actual intention!