1
votes

I'm currently deploying this cloud function to my firebase app and I will be using the Node v8 runtime, so I can use the async/await syntax.

I'm having some trouble handling different kinds of errors that may occur in the same function.

When completed, the function should receive a url parameter to make request to that url, scrape the response body for some data and save it to the database. At this point, it is just returning the same url string that it received for testing purposes.

So far, this is what I have:

const functions = require('firebase-functions');
const request = require('request');
const cheerio = require('cheerio');

exports.getDataFromUrl = functions.https.onCall((data) => {

  // PROMISIFIED REQUEST TO USE WITH ASYNC AWAIT
  const promisifiedRequest = function(options) {
    return new Promise((resolve,reject) => {
      request(options, (error, response, body) => {
        if (error) {
          return reject(error);
        }
        return resolve(response);
      });
    });
  };

  // CHECK IF URL IS PRESENT, IF NOT, THROW ERROR
  if (!data.url) {
    throw new functions.https.HttpsError('invalid-argument','The URL parameter was invalid.');
  }

  // URL passed from the client.
  const url = data.url;

    // IIFE ASYNC FUNCTION
    (async function() {

      // TRY BLOCK
      try {

        // REQUEST OPTIONS TO THE URL
        const urlOptions = {
          url: 'https://www.someINEXISTENT.url',
          method: 'GET',
          gzip: true,
          headers: {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36'
          },
          jar: true
        };

        // CREATE RESPONSE AND CHEERIO OBJECT
        let response = null;
        let $ = null;

        // SEND REQUEST TO URL, AND PARSE WITH CHEERIO
        response = await promisifiedRequest(urlOptions);
        $ = cheerio.load(response.body);

      } // TRY BLOCK - END

      // CATCH BLOCK
      catch (error) {
        console.log('Caught an error: ' + error);
        throw new functions.https.HttpsError('unknown', error.message, error);
      }

      console.log('End of async function...');

    })()

  return {
    yourUrl : url
  };

});

My first error case, which happens when the URL is invalid is working just fine. The execution stops when I throw the following error:

throw new functions.https.HttpsError('invalid-argument','URL invalid.');

As far as I understood, a need to throw this HttpsError in order to be able to catch in on the client. And it works. I get this error on the client, when it happens.

enter image description here

My problem is on the second type of error, which should be caught by the try/catch statement inside my async function. That error should happen when I try to request an inexistent url, for example.

What is happening is this (picture below):

The catch block is activated and I can see the `console.log() on my function console, but somehow it doesn't throw the error, and it complains about "throwing inside an async function without a catch block", even though I'm trowing it from inside a catch block. My client code does not get this error at all. From my client's perspective, the function completes without any errors.

enter image description here

Error:

error: (node:5160) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async fnction without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)

I have tried adding and outer try/catch blocks. And also using .catch() after the async IIFE to throw the error outside the async function, but it didn't solve the problem.

What am I doing wrong?

1
If you have text of an error message to show, please copy that into the question itself instead of adding a screenshot. Screenshots are difficult to read and impossible to search on SO.Doug Stevenson
Thanks. I'll edit and add that.cbdeveloper

1 Answers

3
votes

...but somehow it doesn't throw the error...

That's because nothing uses the promise the async function creates, which is rejected by the throw. One of the rules of promises is: You must handle promise rejection or return the result to something else that will.

functions.https.onCall allows you to return a promise, so I'd make the callback an async function rather than using an async IIFE. You can't synchronously provide your result or throw an error, because you're dealing with an asynchronous operation.

Something along these lines (may need tweaking, and see *** comments):

const functions = require('firebase-functions');
const request = require('request');
const cheerio = require('cheerio');

// *** No reason to define this within `onCall`'s handler
const promisifiedRequest = function(options) {
  return new Promise((resolve,reject) => {
    request(options, (error, response, body) => {
      if (error) {
        return reject(error);
      }
      return resolve(response);
    });
  });
};

exports.getDataFromUrl = functions.https.onCall(async (data) => { // *** Make it async, since `onCall` allows you to return a promise

  // CHECK IF URL IS PRESENT, IF NOT, THROW ERROR
  if (!data.url) {
    throw new functions.https.HttpsError('invalid-argument','The URL parameter was invalid.');
  }

  // URL passed from the client.
  const url = data.url;

  // *** No need for try/catch unless you want to change the error
  // *** If you do, though, add it back and use `throw` in the `catch` block (as you did originally).
  // *** That will make the promise this async function returns reject.

  // REQUEST OPTIONS TO THE URL
  const urlOptions = {
    url: 'https://www.someINEXISTENT.url',
    method: 'GET',
    gzip: true,
    headers: {
      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36'
    },
    jar: true
  };

  // CREATE RESPONSE AND CHEERIO OBJECT
  let response = null;
  let $ = null;

  // SEND REQUEST TO URL, AND PARSE WITH CHEERIO
  response = await promisifiedRequest(urlOptions);
  $ = cheerio.load(response.body);

  // *** Do you really want to return the URL? Not something from the body of what you requested?
  return {
    yourUrl : url
  };
});