Whether you're making API calls from Node.js or in the browser, connection failures are going to happen eventually. Some request errors are valid. Maybe the endpoint was wrong or the client sent the wrong data. Other times you can be sure that the error is the result of a problem with the connection to the server or one of the many hops in-between. While API and web service monitoring can inform you about the problem, a more active solution can take care of it for you. To fix this, you can improve your HTTP request library by adding intelligent retry functionality. This kind of remediation is crucial to ensuring your API calls are successful. Some libraries, like support retrying failed requests out of the box, while others like require a separate plugin. If your preferred library doesn't support them, this article is for you. We'll explore adding status-code-specific retries to a request, making them incrementally longer with a technique called "back-off", and more. got axios The basics of a retry To decide when to retry a request, we need to consider what to look for. There are a handful of that you can check against. This will let your retry logic differentiate between a failed request that is appropriate to retry—like a gateway error—and one that isn't—like a 404. For our examples, we will use 408, 500, 502, 503, 504, 522, and 524. You could also check for , as long as you incorporate the Retry-After header into the back-off logic. HTTP status codes 429 The next consideration we want is how often to retry. We will start with a delay, then increase it each additional time. This is a concept known as "back-off". The time between requests will grow with each attempt. Finally, we'll also need to decide how many attempts to make before giving up. Here's an example of the logic we'll be using in pseudo-code: If total attempts > attempts, continue if status code type matches, continue if (now - delay) > last attempt, try request else, return to the start We could also check for things like error codes (from Node.js), and limit retrying to certain methods. For example, ignoring POST is often a good idea, to ensure no duplicate entries are created. Recursive request structure To make this all work, we'll be making a request from within a failed request. This requires the use of recursion. Recursion is when a function calls itself. For example, if we wanted to infinitely keep trying to make a request it might look like this: { requests(url, options, response => { (response.ok) { response } { myRequest(url, options) } }) } ( ) function myRequest url, options = {} return if return else return Notice that the block returns the function. Since most modern HTTP request implementations are promise-based, we can return the result. This means that to the end user, the whole process looks like a normal call. For example: else myRequest myRequest( ).then( .log(response)) "https://example.com" console With a plan in mind, let's look at implementing retries in javascript. Add retry to Fetch First, we'll start with the browser's . The fetch implementation will be similar to the recursion example above. Let's implement that same example, but using fetch and a status check. Fetch API { fetch(url, options).then( { (res.ok) res.json() fetchRetry(url, options) }) } ( ) function fetchRetry url, options // Return a fetch request return => res // check if successful. If so, return the response transformed to json if return // else, return a call to fetchRetry return This will work to infinitely retry failed requests. . Note: a return will break out of the current block, so we don't need an else statement after return res.json() Now let's add in a max number of retries. { fetch(url, options) .then( { (res.ok) res.json() (retries > ) { fetchRetry(url, options, retries - ) } { (res) } }) .catch( .error) } ( ) function fetchRetry url, options = {}, retries = 3 return => res if return if 0 return 1 else throw new Error console The code is mostly the same, except we've added a new argument and a new condition. Add the re ries argument to the function, with a default value of 3. Then, rather than automatically calling the function on failure, check if any retries are remaining. If so, call . t fetchRetry The new retries value passed to the next attempt is the current retries minus 1. This ensures that our "loop" decrements, and eventually will stop. Without this, it would run infinitely until the request succeeds. Finally, if is not greater than zero, throw a new error for to handle. retries .catch To give it a try, you can make a request to . For example: https://status-codes.glitch.me/status/400 fetchRetry( ) .then( .log) .catch( .error) "https://status-codes.glitch.me/status/400" console console If you check your network traffic, you should see calls in total. The original, plus three retries. Next, let's add in a check for the status codes we want to retry. four { retryCodes = [ , , , , , , ] fetch(url, options) .then( { (res.ok) res.json() (retries > && retryCodes.includes(res.status)) { fetchRetry(url, options, retries - ) } { (res) } }) .catch( .error) } ( ) function fetchRetry url, options = {}, retries = 3 const 408 500 502 503 504 522 524 return => res if return if 0 return 1 else throw new Error console First, declare an array of status codes we want to check for. You could also add this as part of the configuration, especially if you implemented this as a class with a more formal configuration. Next, the retry condition checks to see if the response's status exists in the array using . If it does, try the request. If not, throw an error. ECMAScript's array.includes() There is one last feature to add. The incremental back-off delay between each request. Let's implement it. { retryCodes = [ , , , , , , ] fetch(url, options) .then( { (res.ok) res.json() (retries > && retryCodes.includes(res.status)) { setTimeout( { fetchRetry(url, options, retries - , backoff * ) }, backoff) } { (res) } }) .catch( .error) } ( ) function fetchRetry url, options = {}, retries = , backoff = 3 300 /* 1 */ const 408 500 502 503 504 522 524 return => res if return if 0 => () /* 2 */ return 1 2 /* 3 */ /* 2 */ else throw new Error console To handle the "wait" mechanic before retrying the request, you can use setTimeout. First, we add our new configuration argument (1). Then, set up the setTimeout and use the backoff value as the delay. Finally, when the retry occurs we also pass in the back-off with a modifier. In this case, backoff * 2. This means each new retry will wait twice as long as the previous. Now if we try out the function by calling fetchRetry('https://status-codes.glitch.me/status/500'), the code will make the first request immediately, the first retry after waiting 300ms, the next 600ms after the first response, and the final attempt 900ms after the second response. You can give it a try with any status code by using https://status-codes.glitch.me/status/${STATUS_CODE}. Further configuration and better options This is a great solution for one-off requests or small apps, but for larger implementations, it could be improved. Building a configurable class (or class-like object) will give you more control and allow for separate settings for each API integration. You could also apply this logic to a or any other remediation pattern. circuit breaker Another option is to use a tool that observes and reacts to anomalies in your API calls. At , our team is building just that. Instead of configuring all of this in code for each API, the Bearer Agent handles it all for you. today and let us know what you think Bearer Give it a try @BearerSH Bonus: Add retry to Node's native module http The fetch implementation above works for the browser, but what about Node.js? You could use a fetch-equivalent library like . To make things interesting, let's look at applying the same concepts above to Node.js' native module. node-fetch http To make things a bit easier, we'll use the shorthand method. The retry logic will stay the same, so check out our article on if you want to perform requests other than GET. http.get making API calls with http.request Before we get started, we'll need to change http.get from event-based to promise-based so we can interact with it the same way we did with fetch. If you are new to promises, they are an underlying concept that modern async implementations use. Every time you use or , you are using promises under the hood. For the purposes of this article, all you need to know is that a promise can resolve or reject—in other words, the code passes or fails. Let's look at some code without any retry logic. .then async/await Here's a basic GET using http.get https = ( ) https.get(url, res => { data = { statusCode } = res (statusCode < || statusCode > ) { (res) } { res.on( , d => { data += d }) res.end( , () => { .log(data) }) } }) let require "https" let "" let if 200 299 throw new Error else "data" "end" console To summarize, it requests a url. If the isn't in a defined "success range" (Fetch has the property to handle this) it throws an error. Otherwise, it builds a response and logs to the console. Let's look at what this looks like "promisified". To make it easier to follow, we'll leave off some of the additional error handling. statusCode ok { ( { https.get(url, res => { data = { statusCode } = res (statusCode < || statusCode > ) { reject( (res)) } { res.on( , d => { data += d }) res.on( , () => { resolve(data) }) } }) }) } ( ) function retryGet url return new Promise ( ) => resolve, reject let "" const if 200 299 Error else "data" "end" The key parts here are: Returning a new Promise on successful actions resolve on errors reject We can then test it by calling . Anything outside the 200 range will show up in our catch, while anything within the range will show up in then. retryGet("https://status-codes.glitch.me/status/500").then(console.log).catch(console.error) Next, let's bring all the logic from the fetch example into . retryGet { retryCodes = [ , , , , , , ] ( { https.get(url, res => { data = { statusCode } = res (statusCode < || statusCode > ) { (retries > && retryCodes.includes(statusCode)) { setTimeout( { retryGet(url, retries - , backoff * ) }, backoff) } { reject( (res)) } } { res.on( , d => { data += d }) res.on( , () => { resolve(data) }) } }) }) } ( ) function retryGet url, retries = , backoff = 3 300 /* 1 */ const 408 500 502 503 504 522 524 /* 2 */ return new Promise ( ) => resolve, reject let "" const if 200 299 if 0 /* 3 */ => () return 1 2 else Error else "data" "end" This is similar to the example. First, set up the new arguments (1). Then, define the . Finally, set up the retry logic and . This ensures that when the user calls and expects a promise back, they will receive it. fetch retryCodes (2) return retryGet retryGet(...) Wrapping up You stuck with it through the bonus section 🎉! Using the same concepts in this article, you can apply retry functionality to your favorite library if it doesn't already include it. Looking for something more substantial? Give Bearer a try, and check out with the Bearer Blog for more on Node.js, API integrations, monitoring best practices, and more. Previously published at https://blog.bearer.sh/add-retry-to-api-calls-javascript-node/