0

I am trying to get a route to wait for an async function in another module to return before render runs, but no matter what I do, res.render always runs first. This is my current code, which actually just freezes and never loads:

router.get('/', function(req, res, next) {
  try {
    const cities = spreadsheet.getData()

  } catch(err) {
    console.log(err)
  }

  res.render('index', { cities: cities})
})

and the function it is waiting for is this:

exports.getData = function () {
  parsedData = [];
  accessSpreadsheet().then(function(data) {
    console.log(parsedData)
    return parsedData;
  });
};

const accessSpreadsheet = async() => {
  await doc.useServiceAccountAuth({
      client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
      private_key: process.env.GOOGLE_PRIVATE_KEY,
    });
  const loadedDoc =  await doc.loadInfo();
  sheet = await doc.sheetsByIndex[0];
  const cells = await sheet.loadCells(allCells[cellsIndex]);
  const data = await parseData();
  const moreCells = await checkNextCells()

  return;
}

The render runs first, and the parsedData prints in the console. I also tried making the route async, and I tried res.render inside a callback. Is there any way to make this work?

Ando
  • 165
  • 1
  • 14
  • 1
    You need to put a `return` in front of `accessSpreadsheet().then(...)`, then chain `spreadsheet.getData().then(...)` and put the rest of your logic for the router inside the callback. – Patrick Roberts May 14 '20 at 14:21
  • Sorry, what would the callback look like in `exports.getData()` in this? I tried this before, but `spreadsheet.getData(data).then(res.render('index', { cities: data}))` is `data` undefined, so I think I have the callback wrong. – Ando May 14 '20 at 14:55

2 Answers2

2

Since accessSpreadSheet is an async function, you either need to await or return the promise (as suggested by Patrick Roberts in the comment), in getData function, and similarly in the router.

Using async await you can update your code as below (not tested)

exports.getData = async function () {
  parsedData = [];
  parsedData = await accessSpreadSheet();
  return parsedData;
};

const accessSpreadsheet = async() => {
  await doc.useServiceAccountAuth({
      client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
      private_key: process.env.GOOGLE_PRIVATE_KEY,
    });
  const loadedDoc =  await doc.loadInfo();
  sheet = await doc.sheetsByIndex[0];
  const cells = await sheet.loadCells(allCells[cellsIndex]);
  const data = await parseData();
  const moreCells = await checkNextCells()

  return;
}

And in the router

router.get('/', async function(req, res, next) {
  let cities;
  try {
    cities = await spreadsheet.getData()

  } catch(err) {
    console.log(err)
  }

  res.render('index', { cities: cities})
})
Rachit Anand
  • 616
  • 5
  • 16
  • Does this make `cities` in the router a Promise rather than the data object? I am getting the error `UnhandledPromiseRejectionWarning: ReferenceError: cities is not defined` – Ando May 14 '20 at 14:48
  • 1
    You have a syntax error. `getData` needs to be `async` in order to use the `await` keyword. – Patrick Roberts May 14 '20 at 14:57
  • 2
    @Ando change `const` to `var`. The `const` declaration is block-scope, so the reference is not available outside the `try { ... }` block, whereas `var` is function scope. _Or_ move the `res.render()` inside the `try { ... }` block, and do `res.sendStatus(500)` in the `catch` block. – Patrick Roberts May 14 '20 at 14:58
  • Updating my answer. You get cities is not defined, because cities is defined inside a block. Also as pointed out by Patrick, getData needs to be async. – Rachit Anand May 14 '20 at 14:58
1

In your router, spreadsheet.getData() being async, you need to handle it with async/wait, which will require your callback to be async.

 router.get('/', async function(req, res, next) {
  try {
    const cities = await spreadsheet.getData();

    res.render('index', { cities: cities}) // moved this to inside a try block

  } catch(err) {
    console.log(err)
    // you need to handle the exception here, return error message etc
  }

})

In getData, you need to return a promise that will be resolved when called in router.

exports.getData = async function () {
    let parsedData = [];  
   try {
     parsedData = await accessSpreadsheet();
    } catch (exc) {
      // handle exception here
    }
   return parsedData;
};

finally in accessSpreadsheet(), I do not see where you return the data parsed.

  const accessSpreadsheet = async() => {
  try{
  await doc.useServiceAccountAuth({
      client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
      private_key: process.env.GOOGLE_PRIVATE_KEY,
    });
  let loadedDoc =  await doc.loadInfo();
  let sheet = await doc.sheetsByIndex[0];
  let cells = await sheet.loadCells(allCells[cellsIndex]); // cellsIndex is not defined
  let data = await parseData(); // are you to pass the sheet? do not know how parsedData() works
  let moreCells = await checkNextCells()

  return data; // assuming data is what is meant to be returned
} catch(exc) {
   // probably return null or re-raise exception
}
}

It is important to always use try/catch or then/catch when dealing with async code in NodeJS.

Hope it sheds some light!

artfulbeest
  • 1,395
  • 2
  • 16
  • 22