'Async/Await function failing

I'm trying to build a nodeJS script that pulls records from an Airtable base, bumps a UPC list up against the [UPC Item DB API][1], writes the product description ("Title") and product image array from the API response to an object, and then updates corresponding Airtable records with the pre-formatted using the Airtable API. I can't link directly to the Airtable API for my base, but the "Update Record" should look like this:

{
  record_id: 'myRecord',
  fields: {
    'Product Description': 'J.R. Watkins Gel Hand Soap, Lemon, 11 oz',
    'Reconstituted UPC': '818570001330',
    Images: [
      'https://images.thdstatic.com/productImages/b3e507dc-2d4a-48d4-a469-51a34c454959/svn/j-r-watkins-hand-soaps-23051-64_1000.jpg',
      'http://pics.drugstore.com/prodimg/332476/450.jpg',
    ]
  }
}
var Airtable = require('airtable');
var base = new Airtable({apiKey: 'myKey'}).base('myBase');
var request = require('request');

// Function to slow the code down for easier console watching
function sleep(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

// Function to slow the code down for easier console watching
async function init(x) {
  console.log(1);
  await sleep(x*1000);
  console.log(2);
}


// Big nasty async function
async function imagesToAirtable() {
  ///Run through the airtable list
  /// create the UPC_list that will be updated and pushed to Airtable to update records
  const upc_list = []; 
  
  /// Pull from Airtable and assign array to an object
  const airtable_records = await base('BRAND')
  .select( { maxRecords : 3 })
  .all()

  /// Troubleshooting console.logs
  console.log(airtable_records.length);
  console.log("Entering the FOR loop")
  /// Loop through the list, append req'd fields to the UPC object, and call the UPCItemDB API
  for (var i = 0 ; i< airtable_records.length ; i++) {
    
     /// Push req'd fields to the UPC object
     await upc_list.push(
      { record_id : airtable_records[i].get("Record ID"),
        fields: {
          "Product Description" : "",
          "Reconstituted UPC": airtable_records[i].get("Reconstituted UPC"),
          "Images": []
        }
      }
    );
    
    /// Troubleshooting console.logs
    console.log(upc_list)
    console.log("Break");

        /// call API 
        await request.post({
            uri: 'https://api.upcitemdb.com/prod/trial/lookup',
            headers: {
              "Content-Type": "application/json",
              "key_type": "3scale"
            },
            gzip: true,
            body: "{ \"upc\": \""+airtable_records[i].get("Reconstituted UPC")+"\" }",
          }, /// appending values to upc_list object
            function (err, resp, body) {
                 console.log("This is loop "+ i)
                 upc_list[i].fields["Images"] = JSON.parse(body).items[0].images
                 upc_list[i].fields["Product Description"] = JSON.parse(body).items[0].title
                 console.log(upc_list[i]);
          }
      )}
};

imagesToAirtable();

I haven't gotten to writing the Airtable "Update Record" piece yet because I can't get the API response written to the upc_list array.

I get an error message on the last run of the FOR loop. In this case, the first and second time through the loop work fine and update the upc_list object, but the third time, I get this error:

upc_list[i].fields["Images"] = JSON.parse(body).items[0].images
            ^

TypeError: Cannot read property 'fields' of undefined

I know this has to do with async/await, but I'm just not experienced enough at this point to understand what I need to do.

I also know that this big nasty async/await function should be written into individual functions and then called in one single main() function but I can't figure out how to make everything chain together properly with async/await. Tips on that would be welcome as well :)

I have tried separating FOR loop into two FOR loops. The first for the initial append of the upc_list item, and the second for the API call and append with the parsed response.



Solution 1:[1]

I was going to skip by this question until I saw this:

I also know that this big nasty async/await function should be written into individual functions

You are so right about that. Let's do it!

// get records from any base, up to limit
async function getRecords(base, limit) {
  return base(base)
    .select( { maxRecords : limit })
    .all();
}

// return a new UPC object from an airtable brand record
// note - nothing async is being done here
function upcFromBrandRecord(brand) {
  return {
    record_id: brand.get("Record ID"),
    fields: {
      "Product Description": "",
      "Reconstituted UPC": brand.get("Reconstituted UPC"),
      "Images": []
    }
  };
}

The request module you're using doesn't use promises. There's a promise-using variant, I believe, but without installing anything new, we can "promise-ify" the post method you're using.

async function requestPost(uri, headers, body) {
  return new Promise((resolve, reject) => {
    request.post({ uri: uri, headers, gzip: true, body },
      (err, resp, body) => {
        err ? reject(err) : resolve(body)
      }
    )}
  });
}

Now we can write a particular one for your usage...

async function upcLookup(brand) {
  const uri = 'https://api.upcitemdb.com/prod/trial/lookup';
  const headers =  {
   "Content-Type": "application/json",
   // probably need an api key in here
   "key_type": "3scale"
  };
  const body = JSON.stringify({ upc: brand.get("Reconstituted UPC") });
  const responseBody = await requestPost(uri, headers, body);
  // not sure if you must parse, but copying the OP
  return JSON.parse(responseBody);
}

For a given brand record, build a complete upc record by creating the structure and calling the upc api...

async function brandToUPC(brand) {
  const result = upcFromBrandRecord(brand);
  const upcData = await upcLookup(brand);

  result.fields["Images"] = upcData.items[0].images;
  result.fields["Product Description"] = upcData.items[0].title;
  return result;
}

Now we have all the tools needed to write the OP function simply...

// big and nasty no more!
async function imagesToAirtable() {
  try {
    const airtable_records = await getRecords('BRAND', 3);
    const promises = airtable_records.map(brandToUPC);
    const upc_list = await Promise.all(promises);  //edit: forgot await
    console.log(upc_list);
  } catch (err) {
    // handle error here
  }
}

That's it. Caveat. I haven't parsed this code, and I know little or nothing about the services you're using, or whether there was a bug hidden underneath the one you've been encountering. So it seems unlikely that this will run out of the box. What I hope I've done is demonstrate the value of decomposition for making nastiness disappear.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1