13
votes

So I've happily been using async/await since node 8 is supported on Firebase Cloud Functions. I am struggling with 1 thing though. When using callable functions, it is told that you have to return a promise in the function, otherwise it won't work correctly. When using raw promises, its clear to me how to use it:

exports.createBankAccount = functions.region('europe-west1').https.onCall((data, context) => {
    return promiseMethod().then((result) => {
        return nextPromise(result);
    }).then((result) => {
        return result;
    }).catch((err) => {
        // handle err
    })
});

But now, with async await, I'm not sure how to return this "chain of promises":

exports.createBankAccount = functions.region('europe-west1').https.onCall(async (data, context) => {
    const res1 = await promiseMethod();
    const res2 = await nextPromise(res1);
    return res2;
    // ??? Where to return the promise?
});

Does somebody know?

10
"I'm not sure how to return this "chain of promises":" — Exactly as you have done in the code you wrote.Quentin
Alright, but the logs on the firebase console tells me that I am calling the function 2 times, when I am actually calling it only 1 time. I thought this had something to do with returning these promises the wrong way.Jaap Weijland
You can easily set up an example project and verify the claims of the answers. Just create a function which will wait 10 Seconds and then return a result. If you call this function with await, the firebase function will correctly wait 10 seconds and then return the result.Falco
I have added an authorative code-sample by the google-firebase team to my answer. If that is not enough to support it, I don't know.Falco

10 Answers

3
votes

HTTP functions don't return a promise. They just send a result. You still have to use promises correctly in order to send the result, but a return value is not required. HTTP functions are terminated when the response is sent. See the documentation for more details:

Terminate HTTP functions with res.redirect(), res.send(), or res.end().

1
votes

You nailed it with your example code.

Async/await is just a newer way of promise. They can be used interchangeable.

Here is an example promise and async/await of the same function.

This

exports.createBankAccount = functions.region('europe-west1').https.onCall((data, context) => {
    return promiseMethod().then((result) => {
        return nextPromise(result);
    }).catch((err) => {
        // handle error here
    })
});

is equivalent to this:

exports.createBankAccount = functions.region('europe-west1').https.onCall(async (data, context) => {
  try {
    const result = await promiseMethod();
    return nextPromise(result); // You are returning a promise here
  }catch(e) {
    // handle error here
  }
});

Note that in both cases, you are returning a promise at the end. The return value of this onCall function would be whatever nextPromise(result) is. Since you are returning nextPromsie(result), you don't need to await it.

1
votes

To see the code solution to your question look at the answer of dshukertjr.

If you want to understand how to return a "chain of promises" with async/await, here is your answer:

You cant ! Why ? Because await is used to wait for a Promise to complete. Once await return a value their is no more Promise.

So if you absolutely want to return a promise using await, you can wait for one of the two functions that return promises but not both.

Here is two way to do that:

A :

exports.createBankAccount = functions.region('europe-west1').https.onCall(async (data, context) => {
    try {
        const result = await promiseMethod();
        return nextPromise(result); // You are returning a promise here
    }catch(e) {
        // handle error here
    }
});

B:

exports.createBankAccount = functions.region('europe-west1').https.onCall(async (data, context) => {
    return promiseMethod().then(async (result) => {
        return await nextPromise(result);
    }).catch((err) => {
        // handle err
    })
});

The only difference between A and B is that A waits for "PromiseMethod" to complete before returning a Promise. Whereas B returns a Promise right after being called.

0
votes

Seems, we have to wait for several Promises in way like this:

const listOfAsyncJobs = [];
listOfAsyncJobs.push(createThumbnail(1, ...));
listOfAsyncJobs.push(createThumbnail(2, ...));
listOfAsyncJobs.push(createThumbnail(3, ...));
...
return Promise.all(listOfAsyncJobs); // This will ensure we wait for the end of the three aync tasks above.
0
votes

From async method whatever you return it gets wrapped in promise automatically. e.g

const myFun = async () => {return 5}

 myFun();


// Output in the console
Promise {<fulfilled>: 5}

And you can chain with the returned result since it is a promise

Another example with enhancement as suggested in other answer

 const myFun4 = async () => {
      const myNum = await new Promise(r => window.setTimeout(() => r(5), 1000));
      const myNum2 = await new Promise(r => window.setTimeout(() => r(5), 1000));
      return myNum + myNum2;
    }
    myFun4().then((n) => console.log(n));
    // Output
    10
0
votes

"await" is just syntax sugar for returning a Promise

When you write an async function, the code will actually exit the function and return a Promise at the first await it encounters. All code after the await will be converted to a then().

So for firebase writing code with async/await is perfectly save and in my experience even less error-prone, since I can more easily structure try&catch in my code!

Proof:

Just run this in your console:

async function iAmAsync() {
  await new Promise(r => window.setTimeout(r, 1000))
  return 'result'
}

let x = iAmAsync()
console.log(x)

Will print: Promise{<resolved>: "result"}

TL;DR: You don't need to change anything - if you write code with multiple awaits, this will be handled by firebase like a chain of promises and everything will just work.

And since my answer was downvoted, here is an authorative code-sample by the google firebase team itself:

https://github.com/firebase/functions-samples/blob/master/quickstarts/uppercase/functions/index.js

exports.addMessage = functions.https.onRequest(async (req, res) => {
// [END addMessageTrigger]
  // Grab the text parameter.
  const original = req.query.text;
  // [START adminSdkPush]
  // Push the new message into the Realtime Database using the Firebase Admin SDK.
  const snapshot = await admin.database().ref('/messages').push({original: original});
  // Redirect with 303 SEE OTHER to the URL of the pushed object in the Firebase console.
  res.redirect(303, snapshot.ref.toString());
  // [END adminSdkPush]
});
0
votes

The return value of async-await function is Promise. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function#return_value

So, what you did actually is returning a chain of promises.

const nextPromise = () => {
    console.log('next promise!');
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('next promise result')
        }, 3000)
    });
}

const promiseMethod = () => {
    console.log('promise!');
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('promise result');
        }, 2000)
    });
}

exports.createBankAccount = functions.https.onCall((data, context) => {
    return promiseMethod().then((result) => {
        return nextPromise(result);
    }).then((result) => {
        return result;
    }).catch((err) => {
        // handle err
        console.log(err);
    })
});


exports.createBankAccountAsync = functions.https.onCall(async (data, context) => {
    const result = await promiseMethod();
    const res = await nextPromise(result);
    return res;
});

I have created test project on firebase and both function calls give same logs. enter image description here

0
votes

A solution in that case is Promise.all().

    exports.createBankAccount = functions.region('europe-west1').https.onCall(async (data, context) => {
    const promises = [];

    const res1 = await promiseMethod();
    const res2 = await nextPromise(res1);

    promises.push(res1);
    promises.push(res2);

    // Here's the return of the promises
    return Promise.all(promises).catch(error => console.error(error));
});

You may find more informations about promises in this article on freecodecamp.org/promise-all

0
votes

Since you need to return promise you can create the promise object and resolve/reject (return) your response from api after processing all the promises.

Option 1:

exports.createBankAccount = functions.region('europe-west1').https.onCall((data, context) => {
    return new Promise(async (resolve, reject) => {
        try {
            const res1 = await promiseMethod();
            const res2 = await nextPromise(res1);
            // This will return response from api
            resolve(res2);
        }
        catch (err) {
            // Handle error here
            // This will return error from api
            reject(err)
        }
    })
});

Option 2:

exports.createBankAccount = functions.region('europe-west1').https.onCall((data, context) => {
    return new Promise(async (resolve, reject) => {
        const res1 = await promiseMethod();
        const res2 = await nextPromise(res1);
        // This will return response from api
        resolve(res2);
    })
        .then((val) => val)
        .catch((err) => {
            // Handle error here
            // This will return error from api
            return err
        })
});
-2
votes

Just convert to a Promise if required.

I.e. If nextPromise returns a Promise:

exports.createBankAccount = functions.region('europe-west1').https.onCall(async (data, context) => {
    const res1 = await promiseMethod();
    return nextPromise(res1);
});

On the other hand, if nextPromise is an async function, just convert it to a Promise:

exports.createBankAccount = functions.region('europe-west1').https.onCall(async (data, context) => {
    const res1 = await promiseMethod();
    return Promise.resolve(nextPromise(res1));
});

you can also convert the result:

exports.createBankAccount = functions.region('europe-west1').https.onCall(async (data, context) => {
    const res1 = await promiseMethod();
    const res2 = await nextPromise(res1);
    return Promise.resolve(res2);
});