Retrying and Exponential Backoff with Promises
Suggested Reading: Sequential and Parallel Asynchronous Functions
When you don't have an interface for knowing when a remote resource is available, an option to consider is to use exponential backoff rather than to poll that resource until you get a response.
Set Up
In this scenario, let's mimic the behaviour of a browser and a server. Let's say the server has an abysmal failure rate of 80%.
const randomlyFail = (resolve, reject) =>
Math.random() < 0.8 ? reject() : resolve();
const apiCall = () =>
new Promise((...args) => setTimeout(() => randomlyFail(...args), 1000));
The apiCall
function mimics the behaviour of calling an endpoint on a server.
Retrying
When the apiCall
is rejected, getResource
is called again immediately.
const getResource = () => apiCall().catch(getResource);
With a Delay
If a server is already failing, it may not be wise to overwhelm it with requests. Adding a delay to the system can be a provisional solution to the problem. If possible, it would be better to investigate the cause of the server failing.
const delay = () => new Promise(resolve => setTimeout(resolve, 1000));
const getResource = () =>
apiCall().catch(() => delay().then(() => getResource()));
Exponential Backoff
The severity of a server's failure is sometimes unknown. A server could have had a temporary error or could be offline completely. It can be beneficial to increase the retry delay with every attempt.
The retry count is passed to the delay function and is used to set the setTimeout
delay.
const delay = retryCount =>
new Promise(resolve => setTimeout(resolve, 10 ** retryCount));
const getResource = (retryCount = 0) =>
apiCall().catch(() => delay(retryCount).then(() => getResource(retryCount + 1)));
In this case was used as the timeout where is the retry count. In other words the first 5 retries will have the following delays: .
Using async/await
and Adding a Retry Limit
The above can also be written in a more intelligible form using async/await
. A retry limit was also added to restrict the maximum wait time for the getResource
function.
Since we now have a retry limit, we should have a way to propagate the error, so a lastError
parameter was added.
const getResource = async (retryCount = 0, lastError = null) => {
if (retryCount > 5) throw new Error(lastError);
try {
return apiCall();
} catch (e) {
await delay(retryCount);
return getResource(retryCount + 1, e);
}
};
Conclusion
Exponential backoff is a useful technique in dealing with unpredictable API responses. Each project will have its own set of requirements and constraints, and there may be circumstances where there are better solutions. My hope is that, if exponential backoff is the right solution, you'll know how to implement it.