Reputation: 406
I want some JavaScript code to take 3 things as parameters:
What I ended up doing is using a for
loop. I did not want to use a recursive function : this way, even if there are 50 attempts the call stack isn't 50 lines longer.
Here is the typescript version of the code:
/**
* @async
* @function tryNTimes<T> Tries to resolve a {@link Promise<T>} N times, with a delay between each attempt.
* @param {Object} options Options for the attempts.
* @param {() => Promise<T>} options.toTry The {@link Promise<T>} to try to resolve.
* @param {number} [options.times=5] The maximum number of attempts (must be greater than 0).
* @param {number} [options.interval=1] The interval of time between each attempt in seconds.
* @returns {Promise<T>} The resolution of the {@link Promise<T>}.
*/
export async function tryNTimes<T>(
{
toTry,
times = 5,
interval = 1,
}:
{
toTry: () => Promise<T>,
times?: number,
interval?: number,
}
): Promise<T> {
if (times < 1) throw new Error(`Bad argument: 'times' must be greater than 0, but ${times} was received.`);
let attemptCount: number;
for (attemptCount = 1; attemptCount <= times; attemptCount++) {
let error: boolean = false;
const result = await toTry().catch((reason) => {
error = true;
return reason;
});
if (error) {
if (attemptCount < times) await delay(interval);
else return Promise.reject(result);
}
else return result;
}
}
The delay
function used above is a promisified timeout:
/**
* @function delay Delays the execution of an action.
* @param {number} time The time to wait in seconds.
* @returns {Promise<void>}
*/
export function delay(time: number): Promise<void> {
return new Promise<void>((resolve) => setTimeout(resolve, time * 1000));
}
To clarify: the code above works, I'm only wondering if this is a "good" way of doing it, and if not, how I could improve it.
Any suggestion? Thanks in advance for your help.
Upvotes: 7
Views: 14030
Reputation: 107
Check this pure javascript async-retry library out.
Example
const { retry } = require('@ajimae/retry')
function exec() {
// This will be any async or sync action that needs to be retried.
return new Promise(resolve => {
setTimeout(() => {
resolve({ message: 'some async data' })
}, 1500)
})
}
// takes the response from the exec function and check if the condition/conditions are met
function predicate(response, retryCount) => {
console.log(retryCount) // goes from 0 to maxRetries
// once this condition is met the retry exits
return (response == 200)
}
(async function main() {
// enable or disable an exponential backoff behaviour if needed.
const result = await retry(exec, predicate, { maxRetries: 5, backoff: true })
console.log(result) // { message: 'some async data' }
})()
PS: I authored this library.
Upvotes: 0
Reputation: 31
Here is some code that works.
Helper
interface RetryProps {
attempts?: number
delay: number
fn: () => boolean
maxAttempts: number
}
function retry({ fn, maxAttempts = 1, delay = 1000, attempts = 5 }: RetryProps) {
return new Promise((resolve, reject) => {
if (fn()) resolve(true)
else {
if (attempts < maxAttempts) {
setTimeout(
() =>
retry({ fn, maxAttempts, delay, attempts: attempts + 1 })
.then(() => resolve(true))
.catch((err) => reject(err)),
delay
)
} else reject('Could not resolve function.')
}
})
}
Then pass it a function that returns true
when successful.
Usage example
retry({
fn: function () {
// Whatever you want to test, return true upon success.
const elExists = !document.getElementById('myRandomELement')
console.log('Element present in DOM?', elExists)
return elExists
},
maxAttempts: 4,
delay: 2000,
})
.then(() => console.log('Done'))
.catch(() => console.log("Didn't pan out"))
Since it returns a promise you can await
it or use then/catch
to settle it.
Upvotes: 0
Reputation: 71
Using recursive functions with Promises won't be an issue with the callstack as the Promise is returned instantly and the then
or catch
function will be called after an asynchronous event.
A simple javascript function would be like:
function wait (ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
function retry (fn, maxAttempts = 1, delay = 0, attempts = 0) {
return Promise.resolve()
.then(fn)
.catch(err => {
if (attempts < maxAttempts) {
return retry (fn, maxAttempts, delay, attempts + 1)
}
throw err
})
}
Upvotes: 2
Reputation: 664297
I did not want to use a recursive function: this way, even if there are 50 attempts the call stack isn't 50 lines longer.
That's not a good excuse. The call stack doesn't overflow from asynchronous calls, and when a recursive solution is more intuitive than an iterative one you should probably go for it.
What I ended up doing is using a
for
loop. Is this a "good" way of doing it, and if not, how I could improve it?
The for
loop is fine. It's a bit weird that it starts at 1
though, 0-based loops are much more idiomatic.
What is not fine however is your weird error handling. That boolean error
flag should have no place in your code. Using .catch()
is fine, but try
/catch
would work just as well and should be preferred.
export async function tryNTimes<T>({ toTry, times = 5, interval = 1}) {
if (times < 1) throw new Error(`Bad argument: 'times' must be greater than 0, but ${times} was received.`);
let attemptCount = 0
while (true) {
try {
const result = await toTry();
return result;
} catch(error) {
if (++attemptCount >= times) throw error;
}
await delay(interval)
}
}
Upvotes: 5
Reputation: 150624
You might want to have a look at async-retry, which does exactly what you need. This package lets you retry async operations, and you can configure (among other things) timeouts between retries (even with increasing factors), maximum number of retries, …
This way you don't have to reinvent the wheel, but can rely on a proven package that is being widely used in the community.
Upvotes: 3
Reputation: 1759
Have you considered RxJS?
It is excellent for implementing this sort of logic in async workflows.
Below is an example of how you would do this without breaking your public api (ie. converting from Promise to Observable and back). In practice you would probably want to use either RxJS or Promises in any given project rather than mixing them.
/**
* @async
* @function tryNTimes<T> Tries to resolve a {@link Promise<T>} N times, with a delay between each attempt.
* @param {Object} options Options for the attempts.
* @param {() => Promise<T>} options.toTry The {@link Promise<T>} to try to resolve.
* @param {number} [options.times=5] The maximum number of attempts (must be greater than 0).
* @param {number} [options.interval=1] The interval of time between each attempt in seconds.
* @returns {Promise<T>} The resolution of the {@link Promise<T>}.
*/
export async function tryNTimes<T>(
{
toTry,
times = 5,
interval = 1,
}:
{
toTry: () => Promise<T>,
times?: number,
interval?: number,
}
): Promise<T> {
if (times < 1) throw new Error(`Bad argument: 'times' must be greater than 0, but ${times} was received.`);
let attemptCount: number;
return from(toTry)
.pipe(
retryWhen(errors =>
errors.pipe(
delay(interval * 1000),
take(times - 1)
)
)
)
.toPromise();
}
It may not be worth adding a whole library for this one piece of logic, but if your project involves a lot of complex async workflows such as this, then RxJS is great.
Upvotes: 0