DOUG STEVENSON: Hey there.
Welcome back to another Firecast about Cloud Functions
for Firebase.
My name is Doug Stevenson, and today's topic
is programming with JavaScript Promises in an HTTP
trigger in Cloud Functions.
If you're going to write code for Cloud Functions,
you definitely need to know about how
Promises work, because nearly all the functions you
write will use them.
And if you don't do it correctly,
your code may fail in mysterious ways.
So let's get acquainted with JavaScript Promises, shall we?
A promise represents some asynchronous work that
should eventually complete.
If you're an Android developer, this
is very similar to the task object
that you use sometimes when dealing with the Firebase
Android SDKs.
When the work tracked by a Promise is still executing,
the Promise is in a pending state.
Then, if the work completes successfully,
the Promise is said to be fulfilled.
Or if there's an error, the Promise becomes rejected.
These are the only three states that a Promise can be in--
pending, fulfilled, or rejected.
And once the Promise becomes fulfilled or rejected,
it can never change state again.
Many JavaScript APIs you're going to use
will return a Promise when you call a function to perform
some work.
In particular, the Firebase Admin SDK
will return a promise when you ask
it to do things such as read or write from a database,
interact with files in Cloud Storage,
send a notification with Cloud Messaging,
or update an authenticated user profile.
Any API that returns Promise will do so immediately
before the work is complete.
This is very important to remember,
because you need to use this Promise to figure out
when the work is done.
Only when all the work is done is
it safe to terminate your function,
and it's crucial not to let your function terminate
while work is still going on.
So how exactly do you terminate a Cloud Function correctly?
Well, there's two rules to know.
First, for HTTP type triggers, they're
terminated after they send a response
to the client using the response object that it received.
If you followed my last video, you saw that
with a Hello World function.
Second, for all other types of triggers,
called background triggers, you need
to return a Promise that's fulfilled
only after the work that was started in the function
is fully complete.
If there's no work to wait on, you can just return null.
But you can't leave any Promises dangling in your function
without proper handling.
Otherwise, your pending work may not
complete the way you expect.
When you return a Promise from a background function like this,
it tells Cloud Functions to wait until the Promise becomes
fulfilled or rejected before moving on and cleaning up
that function.
If you return any other type of value,
Cloud Functions will ignore it and terminate the function
immediately.
All right.
Enough talk.
Let's look at some code.
Today, I'll show you an HTTP function.
Here's that Hello World function from the last video.
It's written in TypeScript, which
is very similar to JavaScript, but it's not very useful.
So let's change that.
Say I want to make this function return
the contents of a document in my Firestore database
to the client, kind of like a simple web API.
In the Firebase console for my project,
you can see I have a collection of documents called
cities-weather, where each document tracks
the current weather conditions over time in a city.
I'm going to Boston soon, so I want
to make an HTTP API endpoint that anyone
can call that returns the weather in Boston.
Easy enough with Cloud Functions.
Back to VS Code, the first thing I'll do here
is rename the function.
Then I'll import the Firebase Admin SDK.
If you remember, that module was already installed in my project
when I created it.
The Admin SDK needs to be initialized before use,
so I'll do that.
Then I'll use it in my function to get the document I want.
I know the path of it already, so this is easy.
Notice that VS Code is automatically giving me
some information about the get method,
and I see it returns a Promise that
contains a document snapshot type object
with the data I'm looking for.
So what I need to do here is use that Promise
to get a snapshot of the Boston weather document.
Promises have a method called then
that lets your code continue after the promise is fulfilled
with the results of the work that just finished.
It takes a callback function that's
invoked with those results as its only argument.
We can see here that VS Code is confirming
that the callback passed to then will give me
a document snapshot object as the argument
to the function I'll write.
Inside this callback function, I'll take the snapshot
and convert it to a JavaScript object.
And with that, I'll send its JSON representation
to the client.
The response object knows how to convert the object
to JSON for me.
Lastly, this call to send the response terminates
the function.
Now, I want to test this code I just wrote.
In my last video, you saw that I used
the Firebase CLI to deploy my code to Cloud Functions
to test it.
But that's not always the easiest thing to do.
Most of the time, it's easier to test your code on your computer
before you deploy it.
This can be a whole lot faster than deploying
the function every time you make a change to it.
So instead of deploying, I'll run it locally
on my computer with the Firebase CLI.
For HTTP type functions, I can emulate them like this.
Before I can emulate the HTTP function,
I need to compile that TypeScript into JavaScript.
I'll do that at the command line.
First, I'll change to my project functions folder.
Here, I'll run TSLint on my code to check for possible errors
with npm run script lint.
And what do you know?
There's a problem.
It's saying there's an error where I'm not handling Promises
correctly.
It's giving me the line number, so I'll go there to look.
Back in VS Code, what I'm remembering now
is that I should be thinking about what
happens if this Promise might be rejected with an error.
Since a Promise can be rejected when there's an error,
it's important to think about whether or not
you need to handle the error cases any time you're
working with a Promise.
I think that's important here.
So I'll use the catch function on the Promise
to trap any errors.
It's important to know that the then method also
returns a Promise that's fulfilled or rejected
with all the prior work.
So I'll get a hold of that.
What I'll do is call the catch method on this Promise
to trap anything that goes wrong with the Firestore get method,
and also inside the then callback.
If there's an error in either function,
the catch callback will be invoked with an object that
describes the error.
What I want to do here is log the error to the Firebase
console, and also send an HTTP error response to the client.
You probably want to send a custom error response,
instead of whatever the error message happens to be here.
So this is just a start.
You should be aware that the error message may contain
private information, and you don't want
to send that to any client.
OK.
I'll run the linter again, compile the code,
and emulate the function.
Hopefully, for real this time.
First, I'll run my npm run script lint again
from my shell history.
Everything checks out.
Then, I'll run npm run script build
to compile the TypeScript.
This runs the build command defined in the packaged .JSON
file that Firebase init set up for me.
It executes the TypeScript compiler, or TSC,
to convert my TypeScript into JavaScript.
The converted JavaScript appears in the lib folder,
next to source.
You can see it looks a lot like the original TypeScript,
but I think you'll agree that the TypeScript is easier
to read.
Now, I'll use the Firebase CLI to emulate this function.
I'll run the command, Firebase serve only functions,
to kick that off.
You can see that it gives me a URL
to my function on localhost, my personal machine,
to invoke the function.
So what I'll do is copy the URL, then use curl on the command
line to make an HTTP request to invoke the function.
You may remember last time that I
copied and pasted the function URL into a web browser
to run it.
You can do that, as well.
But sometimes I like to run it this way, too.
And you can see here the JSON contents
of the Boston weather document generated by my function.
In the code I just showed, it's important to note
that I'm sending the full response
to the client in every code path, both success and error.
This is very important.
If you don't send a response to the client,
your function will time out and the client will receive nothing
after that timeout.
That's a pretty bad experience for a web API.
So be absolutely sure you're sending something back
in all cases and all code paths.
OK.
This function works fine, but it's a little bit ugly,
and there are some things I can do to clean it up.
Typically, people chain their Promise functions
together for less typing and better readability.
So I'll do that to tidy up my function right now.
What I'm doing here is removing all the constants
that contain the Promises.
I can simply allow the then and catch methods
to work directly with the return Promises,
without requiring any sort of variable.
This is pretty typical when you have one thing happening
after another in sequence.
So what I have here now is the Promise returned
by get has its then method called,
then the Promise returned by then
has its catch method called.
And now, it's all good.
From now on, I'll write all my Promises like this.
OK.
Now you know the basics of using Promises
in your HTTP functions.
Like I mentioned before, nearly all your functions
will use Promises in some way.
Now, let's review those two rules
for terminating your functions.
First, for HTTP type triggers, you
have to send a response to the client as the very last action
you take in your function for all code
paths in that function, and that's what we did today,
with the help of Promises.
Second, for all other types of triggers,
you have to return a Promise from the function that
becomes fulfilled or rejected when the work is complete.
And that's what I'll show you next time.
And for those of you who know TypeScript pretty well,
you might be wondering when I'll talk about using Async/Await
to deal with Promises.
That's coming in a few videos later in this series.
I personally think it's important to master
normal Promises first, so be sure to subscribe
right here to the Firebase channel
on YouTube to get notified of the next video,
or follow us on Twitter with the name, @Firebase.
Until then, be sure to read the documentation
and other helpful links in the description below.
I'll see you next time.
[MUSIC PLAYING]