1

I want to make a series of ajax requests to a server and then do a final ajax request that uses data I received previously. Obviously, I need to wait for the earlier requests to finish before doing the final request. I'm having trouble implement this in javascript.

I don't want to overwhelm the server, so ideally all requests would be done sequentially.

My simple test code is as follows (replacing web requests with a sleep):

const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay));

var urls = ['1', '2', '3'];

const slowFunc = () => {

    urls.forEach(async (url) => {

        //Don't change this section!
        console.log("a"+url);
        await sleep(5000);
        console.log("b"+url); //I want this to run before c
    });

};

slowFunc();
console.log("c");

This prints "c" before by sleep is finished, which is wrong. How can I get the output to be as follows?

a1
b1
a2
b2
a3
b3
c

Out of interest, how would I get this output? (The exact ordering within the a and b section is unimportant.)

a1
a2
a3
b1
b2
b3
c

I tried reading ES2018: asynchronous iteration but it blew my mind.

Update: I quickly had second thoughts about my example, so here is a better one (that still doesn't work):

var urls = ['https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.2.0/purify.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.8.3/system.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/slim-select/1.18.6/slimselect.min.js'];
var results = {};

const webRequest = (url) => {
    $.ajax({
        type: "GET",
        url: url,
    }).then(data => {
        results[url] = data;
        console.log("b"+url+","+results[url]); //I want this to run before c
    });
}

const slowFunc = () => {

    urls.forEach((url) => {
        console.log("a"+url);
        webRequest(url);
    });

};

slowFunc();
console.log("c");

Thanks for comments so far.

Update 2: Solution to the web request problem, based on Antonio Della Fortuna's advice:

var urls = ['https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.2.0/purify.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.8.3/system.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/slim-select/1.18.6/slimselect.min.js'];
var results = {};

const webRequest = (url) => {
    return new Promise((resolve, reject) => {

        $.ajax({
            type: "GET",
            url: url,
            error: function (data, status, er) { 
                console.log("b,"+url+",failed");
                resolve(); 
            },
        }).then(data => {
            results[url] = data;
            console.log("b,"+url+","+results[url]); //I want this to run before c
            resolve();
        });
    });
}

const slowFunc = async () => {

    for (let i = 0; i < urls.length; i++)
    {
        var url = urls[i];
        console.log("a,"+url);
        await webRequest(url);
    };

};

slowFunc().then(() => {
    console.log("c");
    console.log(results);
})
TimSC
  • 1,459
  • 16
  • 20
  • 5
    `Promise.all` . – Luca Kiebel Jan 28 '22 at 22:03
  • 3
    First and foremost, [don't use `await` in a `.forEach()`](https://stackoverflow.com/questions/37576685/using-async-await-with-a-foreach-loop). – Ivar Jan 28 '22 at 22:05
  • @Ivar if you know it's a duplicate then flag it as such. – pilchard Jan 28 '22 at 22:14
  • I am aware of both of those pages but I've not found a working solution yet. – TimSC Jan 28 '22 at 22:24
  • 1
    @pilchard I'm not _entirely_ convinced it is a dupe. Given that they _also_ want "c" to be printed later, which appears to be in the global scope. Only changing the `.forEach` isn't going to cut it. – Ivar Jan 28 '22 at 22:25
  • 1
    @TimSC If you're already aware of that page, then maybe don't use `await` in a `.forEach()`? That already solves half of your problem. – Ivar Jan 28 '22 at 22:26
  • 1
    By changing the `forEach` to a standard loop the parent function would need to become async and so awaited itself, thus `c` would be printed as expected. The problem seems to be that the OP isn't embracing the fact that once in async land, always in async land. – pilchard Jan 28 '22 at 22:27
  • 1
    @TimSC In addition to `Promise.all` as identified by LucaKiebel in the comments above, you might also want to consider [Promise.allSettled](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled), depending on how you wish to handle the occasional ajax failure... – Trentium Jan 28 '22 at 22:44

2 Answers2

2

There are two ways depending on your use case and you can find the working example here -> https://codesandbox.io/s/zen-carson-ksgzf?file=/src/index.js:569-624:

  1. Parallel Solution: You could run the requests inside the function in parallel and then print "c" like so:
    const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay));

    var urls = ["1", "2", "3"];
    
    const slowFunc = async () => {
      await Promise.all(
        urls.map(async (url) => {
          //Don't change this section!
          console.log("a" + url);
          await sleep(5000);
          console.log("b" + url); //I want this to run before c
        })
      );
    };
    
    slowFunc().then(() => {
      console.log("c");
    });
  1. Sync Solution: You could run all requests as if they were synchronous and wait sequentially:

    const slowFuncSeq = async () => {
      for (const url of urls) {
        //Don't change this section!
        console.log("a" + url);
        await sleep(5000);
        console.log("b" + url); //I want this to run before c
      }
    };

    slowFuncSeq().then(() => {
      console.log("c");
    })

  • Thanks this seems to work. I'll just double check. pilchard you are probably correct but there is quite a bit of content on both pages and I couldn't find the solution. I'm inexperienced in javascript, so what is obvious to you is not obvious to me. – TimSC Jan 28 '22 at 22:39
1

Performing async operations while iterating does not work as you might expect it.

When you do forEach each element will be iterated over synchronously. Thus each element will be iterated over and invoke the callback function, which is why you see the 'a' log first for each element.

The exception to this is using a for...of loop, but for other iterators the await will only be blocking inside the callback function.

If you are attempting to limit the amount of request over time to an API you could implement a leaky bucket algorithm. Or you may refactor your iteration to a for...of loop with your delay function to block requests which maintain sequence but is less optimal as the pace of requests will be your delay time plus the time to finish the other async tasks.

zemaj
  • 330
  • 1
  • 7