1

I'm working with async/await functions in Javascript for the first time. I'm having trouble getting my script to wait for an AJAX response before proceeding and then reading/using that response.

I know there are a lot of questions already on here regarding async/await functions not waiting as intended, but none of the answers to the other questions seemed to work for me.

Basically, what I'm trying to do is loop through an array of layer names (for an OpenLayers map), and forEach layer name I'm sending an AJAX call to retrieve a record (if it exists) from a MySQL database. Then I simply display the result, move on to the next layer name, send the next AJAX call, etc.

Here's my code:

async function getCellLayers() {
    layerNames = [];
    map.getLayers().forEach(function(layer) {
        if (layer.get('type') == "cell") {
            if (layer.getZIndex() == 100) {
                layerNames.push(layer.get('name'));
                if (layerNames.length == 1) {
                    fullExtent = layer.getSource().getExtent();
                } else {
                    ol.extent.extend(fullExtent, layer.getSource().getExtent());
                }
            }
        }
    });
    return layerNames;
}

async function getRecord(cell_date) {
    $.ajax({
        url: 'rec/getRecord/'+cell_date,
        type: 'get',
        dataType: 'json',
        success: await function(response){
          console.log("getRecord response: "+JSON.stringify(response));
          return response['data'];
      }
    });
}

async function testAsyncAwaitFunction() {
    let layerNames = await getCellLayers();
    layerNames.forEach(async function(layerName) {
        cell_date = layerName.substring(3)+"_"+window['currentImage'].substring(17,25);
        console.log(cell_date+":");
        let cellRecord = await getRecord(cell_date);
        console.log("Matches: "+cellRecord.length);
        console.log("testAsyncAwaitFunction response: "+JSON.stringify(cellRecord));
    });
}

I'm expecting to see something like this in the console:

cell101_20190202:
getRecord response: {"data": [{"id":1,"record":"cell101_20190202","value":"0.8"}]}
Matches: 1
testAsyncAwaitFunction response: {"data": [{"id":1,"record":"cell101_20190202","value":"0.8"}]}
cell102_20190202:
getRecord response: {"data": [{"id":2,"record":"cell102_20190202","value":"0.7"}]}
Matches: 1
testAsyncAwaitFunction response: {"data": [{"id":2,"record":"cell102_20190202","value":"0.7"}]}
[ ... and so on ... ]

But instead I'm getting this:

cell101_20190202:
cell102_20190202:
(...)
getRecord response: {"data": [{"id":1,"record":"cell101_20190202","value":"0.8"}]}
getRecord response: {"data": [{"id":2,"record":"cell102_20190202","value":"0.7"}]}
(...)
getRecord response: {"data": [{"id":14,"record":"cell202_20190202","value":"0.6"}]}
(200x) Uncaught (in promise) TypeError: Cannot read property 'length' of undefined
getRecord response: {"data": [{"id":15,"record":"cell204_20190202","value":"0.5"}]}
(...)

I never see the JSON.stringify lines prefixed with testAsyncAwaitFunction response, presumably because the line before that console.log command, which attempts to get the length of cellRecord, fails as the AJAX response hasn't arrived yet.

I suspect the following line will be the key here:

let cellRecord = await getRecord(cell_date);

but I can't work out why that one doesn't seem to be "awaiting" even though this other line a few lines above seems to be working just fine:

let layerNames = await getCellLayers();

Would really appreciate some help from someone with a better grasp of using async/await. I'm much more accustomed to PHP and Python and am having a hard time changing my mindset to thinking asynchronously.

Philosophist
  • 103
  • 1
  • 13
Gareth Jones
  • 517
  • 1
  • 10
  • 25

3 Answers3

6

Two things here: - Your getRecord function does not return a Promisetherefore, the await doesn't wait for anything - forEachcannot work with async functions, as the implementation does not await.

For the first problem you can solve it by doing:

async function getRecord(cell_date) {
    return $.ajax({
        url: 'rec/getRecord/'+cell_date,
        type: 'get',
        dataType: 'json',
    })
    .then(response => response.data);
}

For the second problem, you can do it by running a loop this way:

async function testAsyncAwaitFunction() {
    let layerNames = await getCellLayers();
    for (layerName of layerNames) {

        cell_date = layerName.substring(3)+"_"+window['currentImage'].substring(17,25);
        console.log(cell_date+":");
        let cellRecord = await getRecord(cell_date);
        console.log("Matches: "+cellRecord.length);
        console.log("testAsyncAwaitFunction response: "+JSON.stringify(cellRecord));

    }
}

But doing this makes everything run one by one. You could do even better by sending the requests and then waiting for all of them to complete using Promise.all this way:

const promises = []
for (layerName of layerNames) {
        cell_date = layerName.substring(3)+"_"+window['currentImage'].substring(17,25);
        console.log(cell_date+":");
        promises.push(getRecord(cell_date));
}
const records = await Promise.all(promises)
Francois
  • 3,050
  • 13
  • 21
  • Thanks Francois - this was the solution I ended up using. Bonus points for the last part which of course dramatically improved the speed. – Gareth Jones Mar 24 '19 at 11:06
4

change getRecord to this

function getRecord(cell_date) {
    return $.ajax({
        url: 'rec/getRecord/'+cell_date,
        type: 'get',
        dataType: 'json'
    }).then(function(response){
      console.log("getRecord response: "+JSON.stringify(response));
      return response['data'];
  });
}

And remove both the async and await keywords from everywhere in your code except in the testAsyncAwaitFunction in these two parts:

async function testAsyncAwaitFunction()

and

let cellRecord = await getRecord(cell_date);

Otherwise you don't need them.

It wouldn't have worked before because your function needs to return a promise containing the data. You should read up on JavaScript promises. Async/Await is largely syntactic sugar for these and used to handle async code. The only actual async code you have is the call to getRecord.

ceckenrode
  • 4,543
  • 7
  • 28
  • 48
  • I had done what felt like a lot of reading on promises, but just haven't been able to properly get my head around how they work. That resource you've linked looks much more useful than others I've read. Thanks for your help. – Gareth Jones Mar 24 '19 at 11:08
  • where is 'return response['data']' being returned to? The caller of getRecord? that doesn't make sense to me, since getRecord has its own return statement already. If a .then() function has a return, where is it returned to? – m.arthur Aug 15 '22 at 02:14
1

The thing to remember about async is that any function prepended with async is expected to return a Promise. getRecord should return what you have. Also, while your outer function testAsyncAwaitFunction is async, and your forEach callback is async, you do not have anything waiting for ALL the promises of your forEach to resolve.

You want this pattern:

async function testAsyncAwaitFunction() {
    let layerNames = await getCellLayers();
    const promises = [];
    layerNames.forEach(function(layerName) {
        promises.push(getRecord(cell_date));
    });
    const cell_records = await Promise.all(promises);
    cell_records.forEach(function(cell_record, idx) {
        cell_date = layerNames[idx].substring(3)+"_"+window['currentImage'].substring(17,25);
        console.log(cell_date+":");
        console.log("Matches: "+cellRecord.length);
        console.log("testAsyncAwaitFunction response: "+JSON.stringify(cellRecord));
    })
}
chris
  • 6,653
  • 6
  • 41
  • 54
  • the bluebird library has things that make this easier: http://bluebirdjs.com/docs/api/promise.map.html – chris Mar 24 '19 at 05:20
  • Thanks Chris - that helps me to better understand how promises work. I'll have a look at bluebird too. – Gareth Jones Mar 24 '19 at 11:09