1

I have an Alexa skill that takes the device's address and uses that information to find a nearby facility.

I have taken a look at the solution here as proposed by @jfriend00. My callbacks are structured the same way. I still don't understand why my callbacks aren't returning before the rest of the code runs. I should be seeing the console.info() call from the callback of getJSON(), but it never runs.

EDIT: I clearly don't understand how this async stuff works as my logs are completely out of whack. Can someone please explain to me what is going on? I looked at the solution I linked and my code looks as similar as possible to his barebones solutions. I don't understand where I'm going wrong.

Console logs

enter image description here

getKaiserBuildingHandler()

const getKaiserBuildingHandler = function() {
    console.info("Starting getKaiserBuildingHandler()");
    
    ...

      switch(addressResponse.statusCode) {
          case 200:
              console.log("Address successfully retrieved, now responding to user.");
              const addressObj = addressResponse.address;
              const address = `${addressObj['addressLine1']}, ${addressObj['city']}, ${addressObj['stateOrRegion']} ${addressObj['postalCode']}`;
              var ADDRESS_MESSAGE = Messages.ADDRESS_AVAILABLE;
              const alexa = this;

              getKaiserBuildingHelper(buildingType, address, function(response) {
                ADDRESS_MESSAGE += response;
                alexa.emit(":tell", ADDRESS_MESSAGE);
                console.info("Ending getKaiserBuildingHandler()");
              });

              break;
          
          ...
      }
    });

    ...
};

getKaiserBuildingHelper()

const getKaiserBuildingHelper = function(buildingType, address, callback) {
  console.info("Starting getKaiserBuildingHelper()");

  // var facilityOutput = Messages.ERROR;
  var facilityOutput = "Inside building helper function, initial value.";

  if (buildingType == BLDG_TYPE.PHARMACY ||
      buildingType == BLDG_TYPE.CLINIC ||
      buildingType == BLDG_TYPE.HOSPITAL) {

    ...
    
    facilityOutput = "Before get JSON call.";

    getJSON(buildingType, function(err, data) {
      if (data != "ERROR") {
        console.info("Received data from callback: ", data);
        facilityOutput = "The closest Kaiser Permanente " + buildingType + " to your location is located at " + data + ".";
      } else {
        console.error("Error with data received from callback. ", err);
        facilityOutput = "Entered JSON call, returned ERROR.";
      }
    });
  }

  ...

  callback(facilityOutput);
  console.info("Ending getKaiserBuildingHelper()");
}

getJSON()

const getJSON = function(building, callback) {
  console.info("Starting getJSON()");

  Axios
    .get(getFacilitySearchEndpoint(building))
    .then(function(response) {
      const responseData = response.data.query.search[0].title;
      callback(null, responseData);
      console.info("Data retrieved from Axios call");
      console.info("Ending getJSON()");
    })
    .catch(function(error) {
      callback(error, "ERROR");
      console.info("Error caught from Axios call");
      console.info("Ending getJSON()");
    });
}

getFacilitySearchEndpoint() [wikipedia api just as placeholders]

const getFacilitySearchEndpoint = function(building) {
  console.info("Starting getFacilitySearchEndpoint()");

  switch (building) {
    case BLDG_TYPE.HOSPITAL:
      console.info("Ending getFacilitySearchEndpoint() with " + building);
      return "https://en.wikipedia.org/w/api.php?action=query&format=json&list=search&utf8=1&srsearch=Albert+Einstein";
      break;
    case BLDG_TYPE.PHARMACY:
      console.info("Ending getFacilitySearchEndpoint() with " + building);
      return "https://en.wikipedia.org/w/api.php?action=query&format=json&list=search&utf8=1&srsearch=Harry+Potter";
      break;
    case BLDG_TYPE.CLINIC:
      console.info("Ending getFacilitySearchEndpoint() with " + building);
      return "https://en.wikipedia.org/w/api.php?action=query&format=json&list=search&utf8=1&srsearch=Tony+Stark";
      break;
    default:
      console.info("Ending getFacilitySearchEndpoint() with default");
      break;
  }
  console.info("Ending getFacilitySearchEndpoint()");
}
Mike
  • 1,307
  • 3
  • 17
  • 29
  • Try moving it immediately after `callback(null, response)` when the promise Axios promise is resolved – djfdev Nov 09 '17 at 22:42
  • Although theoretically it should fire as is, but in the wrong order due to how async/promises work – djfdev Nov 09 '17 at 22:42
  • @djfdev, I just made an edit to the code, though not what you were suggesting. Am I on the right track? – Mike Nov 09 '17 at 22:45
  • I think you're misunderstanding how async works. Take a look at Felix King's answer in the link you provided. TLDR async functions (like Axios request, and your `getJSON` wrapper) return immediately, so it looks the new callback you added will run out of order. – djfdev Nov 09 '17 at 22:56
  • I see what you're saying. Based on the logs, the code is returning out of order. What am I misunderstanding about async? – Mike Nov 09 '17 at 22:58
  • @djfdev, apologies, I have made a bunch of new edits – Mike Nov 09 '17 at 23:10

1 Answers1

1

I left some notes in the chat but things might be clearer here. Definitely take another look at Felix's answer in the link you provided, but since you've got a lot of code here it will be easier to understand if I explain in that context.

Axios.get is asynchronous, meaning that it actually returns before the HTTP request is complete. So anything in the body of getJSON that happens after you make the request, will actually happen before the functions passed to then or catch are called:

function getJSON (url, callback) {
  Axios
    .get(url)
    .then(function (data) {
      console.log('I happen later, when the request is finished')
      callback(null, data)
    })
    .catch(function (err) {
      console.log('I happen later, only if there was an error')
      callback(err)
    })

  console.log('I happen immediately!')
}

It's easy to get confused – in asynchronous code, things don't necessarily happen in the order which they appear in the code. So we have to be aware of this when calling asynchronous functions.

This means there is a problem inside of the getKaiserBuildingHelper function. Because your getJSON function is just a callback-style wrapper for the Axios promise, it is also asynchronous. It will return immediately, before the request is complete and the rest of the code inside the function body will continue running as normal.

function getKaiserBuildingHelper (callback) {
  let facilityOutput = 'initial value'

  getJSON('http://some-url.com', function (err, data) {
    console.log('I happen later, when the request is complete')

    if (err) {
      facilityOutput = 'Error!'
    } else {
      facilityOutput = 'Success!'
    }
  })

  console.log('I happen immediately!')
  callback(facilityOutput)
}

This means the last line callback(facilityOutput) happens before the getJSON callback. And as a result, the value of facilityOutput remains unchanged. But you can fix this pretty easily by moving the callback inside of the getJSON callback:

function getKaiserBuildingHelper (callback) {
  getJSON('http://some-url.com', function (err, data) {
    if (err) {
      callback('Error!')
    } else {
      callback('Success!')
    }
  })
}

And now you can use the function as expected:

getKaiserBuildingHelper(function (message) {
  console.log(message) // => 'Success!' or 'Error!'
}

Finally, I'm not sure why you've added this.emit to that callback. Was that something copy pasted from elsewhere?

djfdev
  • 5,747
  • 3
  • 19
  • 38
  • Thank you very much @djfdev, I believe I have a better understanding of async callbacks now. Essentially, my issue was that I was misunderstanding what code ran first with async callbacks. To answer your last question, the `this.emit()` call was added to the callback in `getKaiserBuildingHandler()` because I want it to run __after__ the `response` is returned and added to `ADDRESS_MESSAGE`. – Mike Nov 10 '17 at 20:00
  • However, when `this.emit()` is within the callback, `this` is no longer in the global scope of the application. But wouldn't `this.emit()` run before the callback function runs in `getKaiserBuildingHandler()`? – Mike Nov 10 '17 at 20:02
  • In order to keep the `emit()` call within the callback since that should not fire until it has the necessary information, created a `const alexa = this;` before the callback and then called `alexa.emit()` within the callback. Is this ok practice or is it too "hacky?" – Mike Nov 10 '17 at 20:49
  • I'm a little lost without seeing all the code TBH ... but if it works it works. You might play around with using arrow functions or `Function.prototype.call` to establish a specific context. – djfdev Nov 10 '17 at 20:59
  • I've moved my response into [chat](https://chat.stackoverflow.com/rooms/158643/discussion-between-mike-and-djfdev) if you have a sec. – Mike Nov 10 '17 at 21:08