Recursive HTTP Requests In JavaScript

3 Ways to do Recursive HTTP requests with callbacks, promises, and async/await.

Recursive HTTP Requests With Callbacks, Promises, & Async/Await

Scenario

This will help you if in order for you to perform a task you need to gather a bunch of information in a specific order. Another scenario is that you need to automate a bunch of requests in a particular sequence. I’m going to cover the three ways to do this in a recursive function.

And yes I know there might be a more efficient way to do this GraphQL but that might not solve making multiple queries to multiple APIs.

API

For this tutorial, we’re going to use https://jsonplaceholder.typicode.com as our means to retrieve data.

JSONPlaceholder.typicode.com

The scenario that we’re going to try and solve for is to make 3 requests and retrieve todo 1, 3, and 7, in sequential order.

This is mainly for illustrative purposes, but in a more real-life scenario we might fetching data from our backend to get an address, then sending that request to google maps to get the parsed data, and then making another request to then display that map data.

Callbacks Recursion

Callbacks are the original way to sequentially perform requests. For this one, I’m going to avoid functions that return promises like Fetch and stick with the original XMLHttpRequest.

Defining Our XHR Http Request

const xhr = new XMLHttpRequest();xhr.addEventListener('load', (data) => {
console.log(data.currentTarget.response); // json data
console.log(data.currentTarget.status); // status code
});
xhr.addEventListener('error', (error) => {
// default error handler, ex URL doesn't exist
// Not to be confused with 404 which is still a success
console.log('Something went wrong.');
});
xhr.open('get', 'https://jsonplaceholder.typicode.com/todos/1');xhr.send();

Wrapping Request In Recursive Function

// Recursive Function
const reqs = (requests = [], store = [], failback) => {
// Check if there are still requests to make
if (requests instanceof Array && requests.length > 0) {
const xhr = new XMLHttpRequest();

// Success handling
xhr.addEventListener('load', (data) => {
const status = data.currentTarget.status;
const response = data.currentTarget.response;
if (status === 200) {;
// add to store
store.push(JSON.parse(response));
// remove first request from array of requests
requests.shift();
// move on to next request
return reqs(requests, store, failback);
} else {
if (failback) {
failback(`Request Error: ${status}`);
}
}
});

// Failure handling
xhr.addEventListener('error', (error) => {
if (failback) {
failback('Something went wrong.');
}
});
xhr.open(requests[0].method, requests[0].url);

xhr.send();
} else {
return store;
}
};
// Init
const results = reqs([{method: 'get', url: 'https://jsonplaceholder.typicode.com/todos/1'}], [], (error) => console.log(error));
// Output Results
console.log(results);

This should work right? Well, if we run it, output is:

undefined

Why is this? It’s because our http request hasn’t finished and we already output the result before things are finished.

Final Recursive Callback HTTP Request Code

We need to then change things to make it so that we pass a callback function to let our recursive function know what to call when it’s done.

// Revised Recursive Function
const reqs = (requests = [], store = [], callback, failback) => {
// Check if there are still requests to make
if (requests instanceof Array && requests.length > 0) {
const xhr = new XMLHttpRequest();

// Success handling
xhr.addEventListener('load', (data) => {
const status = data.currentTarget.status;
const response = data.currentTarget.response;
if (status === 200) {;
// add to store
store.push(JSON.parse(response));

// remove first request from array of requests
requests.shift();

// move on to next request
return reqs(requests, store, callback, failback);
} else {
if (failback) {
failback(`Request Error: ${status}`);
}
}
});

// Failure handling
xhr.addEventListener('error', (error) => {
if (failback) {
failback('Something went wrong.');
}
});
xhr.open(requests[0].method, requests[0].url);

xhr.send();
} else {
if (callback) {
callback(store);
}

}
};

// Init
reqs([
{
method: 'get',
url: '
https://jsonplaceholder.typicode.com/todos/1'
},
{
method: 'get',
url: '
https://jsonplaceholder.typicode.com/todos/3'
},
{
method: 'get',
url: '
https://jsonplaceholder.typicode.com/todos/7'
}

],
[],
(data) => console.log(data), // Output results
(error) => console.log(error)
);

Now when we run it, we get the following output:

[
{"userId":1,"id":1,"title":"delectus aut autem","completed":false},{"userId":1,"id":3,"title":"fugiat veniam minus","completed":false},{"userId":1,"id":7,"title":"illo expedita consequatur quia in","completed":false}
]

Promises Recursion

With recursive promises we can take advantage of using fetch. One limit to note with fetch is that we cannot get the progress of the download, so if we wanted to progressing get the data, we wouldn’t be able to do and in that scenario I would just use xhr and wrap it in a new Promise.

Defining Our Fetch Http Request

const config = {
method: 'get'
};
fetch('https://jsonplaceholder.typicode.com/todos/1', config)
.then(response => {
const json = response.json();
if (response.ok) {
return json;
}
return json.then(data => new Promise.reject(data));
})
.then(jsonData => {
console.log(jsonData);
})
.catch(error => console.log(error));

Final Recursive Promise HTTP Requests Code

// Promise Recursive Function 
const reqsPromises = (requests = [], data = []) => new Promise((resolve, reject) => {
if (requests instanceof Array && requests.length > 0) {
const config = {
method: requests[0].method
};

return fetch(requests[0].url, config)
.then(response => {
const json = response.json();
if (response.ok) {
return json;
}
return json.then(data => reject(data));
})
.then(jsonData => resolve(reqsPromises(requests.slice(1), [ ...data, ...[jsonData] ])));
}
return resolve(data);
});
// Init
reqsPromises([
{
url: 'https://jsonplaceholder.typicode.com/todos/1',
method: 'get'
},
{
url: 'https://jsonplaceholder.typicode.com/todos/3',
method: 'get'
},
{
url: 'https://jsonplaceholder.typicode.com/todos/7',
method: 'get'
}
],
[]
)
.then(data => console.log(data)) // Output results
.catch(error => console.log(error));

Async Await Recursion

The thing to remember with await is that it still expects a Promise and fortunately Fetch is already a promise. The main thing to remember is that async is basically turning a regular function into a Promise.

Basically:

const myPromise = () => new Promise.resolve('hello');// = the same as const myAsync = async () => 'hello';

Defining Our Async Await Fetch Http Request

We wrap our Fetch request in a try and catch which acts like a Promise catch and we await the Fetch because it’s already a promise and when we get the response we convert our non promise function into a promise one with async response => ....

try {
const config = {
method: 'get'
};

await fetch(
'https://jsonplaceholder.typicode.com/todos/1',
config
)
.then(async response => {
const json = response.json();
if (response.ok) {
return console.log(await json);
}
// not this object may be empty if no err msg
throw await json;
});
} catch (error) {
console.log('ERROR');
console.log(error);
}

You’ll also notice there is an additional await on json that is because response.json() = json is also a promise.

Final Recursive Async Await HTTP Requests Code

// Async Await Recursive Function 
const reqsAsyncWait = async (requests = [], data = []) => {
if (requests instanceof Array && requests.length > 0) {
const config = {
method: requests[0].method
};

return await fetch(
requests[0].url,
config
)
.then(async response => {
const json = response.json();
if (response.ok) {
const jsonData = await json;
return await reqsAsyncWait(requests.slice(1), [ ...data, ...[jsonData] ]);
}
// not this object may be empty if no err msg
throw await json;
});
}
return data;
};
// Init
try {
console.log(await reqsAsyncWait([
{
url: 'https://jsonplaceholder.typicode.com/todos/1',
method: 'get'
},
{
url: 'https://jsonplaceholder.typicode.com/todos/3',
method: 'get'
},
{
url: 'https://jsonplaceholder.typicode.com/todos/7',
method: 'get'
}
], []));
} catch (error) {
console.log(error);
}

Where To Go From Here

One of the main things that inspired this post was that I couldn’t chain promises for some of my Postman tests, so I had to create a recursive http callback function that performed multiple requests.

If you have control over the API, then I would recommend in most cases that the API data provides everything you need, but in some cases you can’t do that and you will need to perform requests to multiple APIs to all your needed data.

If you got value from this, and/or if you think this can be improved, please let me know in the comments.

Please share it on twitter 🐦 or other social media platforms. Thanks again for reading. 🙏

Follow me on twitter: @codingwithmanny and instagram at @codingwithmanny.

Web Application / Full Stack JavaScript Developer & Aspiring DevOps