Promises Basics
Introduction
Without wasting anymore of our time in discussing abstract ideas, let's now unravel the syntax to create a very simple promise - one that you won't break!
First let's settle on what asynchronous operation we will be carrying out in our promise. Recall from the previous chapter that a promise is meant to simplify writing asynchronous code - so without any doubts we first have to come up with one!
Some quick options are to dispatch an AJAX request, or to set a timer using setTimeout()
.
The latter, as we all know, is both simple to use and understand; and likewise will be the asynchronous operation we'll be carrying out within our promise.
Learn how to work with setTimeout()
in JavaScript Timers.Obviously in practice the async operation will be something more useful like an AJAX request. However for demonstration purposes, it's best to go with setTimeout()
.
So with the task decided let's now dive straight into unravelling it.
Apparently all asynchronous operations complete after a certain amount of time passes, since their execution. For setTimeout()
this time is roughly equal to the value passed as the second argument to the function.
For AJAX requests, although we can't make a rough guess for the time it would take them to complete, we can instead handle the load
event (or analagously the readystatechange
event) to serve this very job.
In simple words, every async operation reaches completion at some point in the future - they do NOT complete rightaway. Callbacks and events are what help us in tracking for these moments, executing given functions on them.
Consider the task of showing a log to the user after 3 seconds. To do so we'll write something like the following:
The async operation here is the counting of the timer, perfomed internally by the browser in the background. The operation completes by calling an anonymous function that makes a console log (most probably after 3 seconds).
Two ideas are showcased in this paragraph that will be used later in transforming the code above into a promise. Can you spot them?
Now with the operation decided it's finally time for the action to begin!
A simple promise
A rule of thumb while working with advanced JavaScript - promises in this case - is to never get panicked seeing any complex piece of code. Just gradually move your eye from statement to statement, decoding the purpose of each one as you do so.
After all, you're now learning advanced stuff!
So keeping this thing in mind, let's now finally see how to create a promise. Consider the following code:
As said before, don't panick on seeing this! Let's understand it gradually, together.
Take note of all the bold phrases in the following section.
Restating what we've said above, it all starts by first deciding on what async operation to execute in the promise. In our case it's setting a timeout of 3 seconds.
With this done, we then encapsulate all the necessary statements required by the operation within a function. For instance, if our async operation is an AJAX request, then we'll simply put all its code inside a function.
Finally to end with, we pass this function to the Promise()
constructor as an argument.
In promise terminology we call this function the executor.
The executor simply executes all the statements that set up an async operation, which is the reason why it's called that.
As soon as a promise is created using new Promise()
along with a function as argument, this function is executed immediately by the promise.
For example, given a function f
, if we call new Promise(f)
, the function f
will be called rightaway as soon as the promise is created.
Calling the executor, obviously, sets up the desired async operation to complete at some point in the future. When it completes, which can be a success or a failure, two special functions pop in.
Notice very closely that it's the Promise()
function that calls the executor internally - you're passing in a reference to a function as an argument to Promise()
which can then call it in any way it likes.
This means that, upon calling the executor, the promise can pass it any arguments it desires.
And in practice two arguments are indeed passed to the executor - one is a function to be called when the async operation completes and the other is a function to be called when the async operation fails.
According to the spec, the former function is called resolve()
while the latter is called reject()
.
Now before we explain the purpose of these functions let's first have a little talk on the whole idea that leads to them.
States of a promise
Recall from the discussion above, that a promise encapsulates a given async operation and that all async operations complete (succeed or fail) at some point in the future, since their execution.
Now when a promise's underlying async operation succeeds or fails how do you think will the promise know of this change? How will the outercode know that this promise is fulfilled or rejected respectively?
This is where the concept of a state comes in.
Essentially, at any point of time, a given promise can be in either one of the three states: pending, fulfilled or rejected.
Pending means that the underlying async operation is still ongoing and no judgements can be made about the promise; fulfilled means that it has succeeded; while rejected means that it has simply failed.
Every promise object has an internal slot, [[PromiseState]]
, according to the spec, to reflect this attribute of the promise. Initially, when we call the Promise()
constructor, it's set to "pending"
.
To get it to change to "resolved"
or similarly "rejected"
we ought to invoke the executor's arguments resolve()
and reject()
respectively.
Promise resolution is done using resolve()
whereas its rejection is done using reject()
.
Without these functions, it would be impossible for us to resolve or reject a promise and likewise perform subsequent actions. These functions don't just change [[PromiseState]]
to "resolved"
or "rejected"
but also complete the promise with a given value.
Let's see what this means...
Value of a promise
Typically when an asynchronous operation completes it is ready with some sort of data to be utilised for further actions.
For example, when an AJAX request completes, it is ready with the data of the response in the responseText
property.
When the async operation succeeds we call this the result of the operation; and similarly when it fails we call this as the reason for the failure.
Now iterating back, if the outercode knows that a promise is resolved then it should also know the value with which it was resolved. Same goes for the failure case as well - if the outercode knows that a promise is rejected then it should also know the reason for its failure.
We refer to this as the value of the promise.
A promise's value is set by passing an argument to the resolve()
or the reject()
function. Internally, these functions put this value into the [[PromiseValue]]
internal slot of the promise.
Think of it naturally - if we are resolving a promise by calling resolve()
, then we shall also be specifying the value with which it's resolved i.e result of the underlying async operation. This can easily be accomplished by providing an argument to .resolve()
.Remember that it's not strictly required to send in an argument to these functions - it's just that when we do send one, the promise is resolved (or rejected) with it - otherwise the value is taken to be undefined
.
Applying this to the code above, when we call resolve("Hello")
in it (on line 3), the promise's status is first set to "resolved"
and then its value is set to "Hello"
.
Now that we are ready with a promise, it's finally time to attach a callback to it, to be invoked once it resolves. This is accomplished using the method then()
. Let's see what it is...
The then()
method
then()
methodThe then()
method, available to all promise instances via Promise.prototype
, is used to execute a function when a promise is resolved or rejected.
It accepts two arguments - quite similar to the ones for the executor: a function to call once the promise is resolved; and a function to call once the promise is rejected.
In the ECMAScript spec, these callbacks are referred to as onFulfilled()
and onRejected()
.
To keep it very simple for now, leaving the detailed aspects of then()
to be discussed in the sections below, just try to understand that then()
queues up callbacks on a promise object, to be fired once it resolves (or rejects).
Always remember that all identifier names in JavaScript have some meanings to them. If you think for a second, the name then
relates well to the English language. For example it is convenient to say that doTheAsyncTask()
and 'then', once it completes do this with the result!Try to make it your habit to find intuition in any new identifier's name. It really helps to understand the purpose of the identifier!
Consider the following extension to our previous code, to eventually make a console log after 3 seconds with the value "Hello"
:
Time to explain what's happening here...
In line 7, by calling then()
on promise
we give it an anonymous callback function to be fired once the underlying async operation completes i.e when the resolve()
parameter is called.
As soon as resolve("Hello")
is called in line 3, roughly after a span of 3 seconds, this anonymous function is invoked with the same argument "Hello"
sent to resolve()
.
This anonymous function logs the given argument, as can be seen in line 8, and thus completes this whole mess of a single promise!
This is the whole anatomy of a simple promise. Yes, this was simple!
Just to recap everything:
A promise object is instantiated by calling
new Promise()
,It's provided with an executor function where some asynchronous operation gets performed,
The operation completes at some point in the future, where it's either resolved or rejected by calling
resolve()
orreject()
respectively,The moment resolution or rejection occurs, the respective callback function sent in via
then()
is fired.
More details will be discussed in the sections below specifically exploring the executor function and the method then()
.
The executor function
While you work with the executor, where an async operation gets executed, you shall know about the technical aspects it utilises under the hood. Let's get an understanding of it.
As we've said earlier, the executor function is fired immediately by the internal engine as soon as the Promise()
constructor is called with one.
This can be confirmed by the code below:
As soon as it's run, this code logs "Hello"
to the console rightaway, which establishes the confirmation.
What follows from this is that since the executor is called immediately, and not queued up on any task queues, it has a synchronous nature, similar to most JavaScript statements.
This means that a long procedure inside the executor, will delay the page render for as long as it doesn't complete till the end. This can be illustrated very easily by using our hand made delay()
function:
If you're on the actual document window then you'll see a blank screen for roughly 5 seconds after you run this code. Otherwise, in the developer tools and on the console tab, you'll see a log after 5 seconds.
Moving on, the next thing to realise while writing the executor is that what we previously called resolve
and reject
are, in effect, parameters of the executor function and hence can be of any name.
For example, we can call them succeed
and fail
; or settle
and dismiss
; or even something short like s
and f
(abbreviations for succeed and fail).
It's simply upto us what we want to go with - resolve
and reject
are just conventional names!
Following we replace the resolve
and reject
parameters with succeed
and fail
respectively:
Since the parameters of the executor have been renamed here, to resolve the promise, we need to call the first succeed
parameter. Don't make the mistake of calling resolve()
- it doesn't exist in the code above!
The final thing left to discuss in this section is that if Promise()
is called without an executor, or if that executor is not a function, an exception is thrown.
This is illustrated as follows:
With the executor function completely scrutinized, it's time to hop into the next concern - then()
.
Then, what?
A detailed discussion on then()
would be incomplete if the resolve()
and reject()
functions are left untouched; likewise following we'll understand the link between the three functions and how they interact with each other via an internal callback queue maintained by the promise.
Appreciate the fact that a promise can be in one of the two states - unsettled or settled - at the time its then()
method is called.
With this in mind, try to think logically on how the method then()
will work in either of these cases.
Unsettled promise
If then()
is called while the promise is still unsettled, what do you think happens?
Well, it's something really interesting!
Guess what happens with the callback function passed to then()
when the method fires at the time its promise is still unsettled.
It is invoked immediately
It is simply ignored
It is queued up internally in the promise
So here's what happens...
By invoking then()
on a promise object, we simply imply that a given function shall be fired when the promise settles. The function is provided as an argument to the method.
Now when the promise is unsettled and we invoke then()
, still we mean exactly the same thing. However the async operation has not completed yet, so how can we fire the callback provided to the method?
We can't! And we can't also just ignore it - otherwise it would be useless to call then()
prior to a promies's settlement.
The only way out is to temporarily save the given callback within the promise and fire it as soon as it is settled (by calling resolve()
or reject()
).
So you ask what's the mechanism of saving the callback? Let's discuss it...
How are the callbacks saved?
Every promise object internally maintains two callback queues - one holding all functions to fire on its resolution and the other holding all functions to fire on its rejection.
Let's call the former: successCallbackQueue
and the latter: failureCallbackQueue
. Initially both these are empty lists.
With the invocation of then()
, however, these lists fill up - the given callback arguments line up into their respective queues one after another. And with the settlement of the promise these queues once again get emptied.
When the resolve()
parameter of the executor is called, it first checks for any callbacks present in successCallbackQueue
; dequeuing and executing them if they exist. Then, as we know, it sets [[PromiseState]]
to "resolved"
and [[PromiseValue]]
to its argument.
On the other hand, reject()
first checks for any callbacks present in failureCallbackQueue
; dequeuing and executing them if they exist; and then sets [[PromiseState]]
to "rejected"
and [[PromiseValue]]
to its argument.
Let's understand all this under the hood of our previous code:
So what's happening over here?
First a new promise is created after executing the given executor, which puts up a timeout to complete, and hence resolve the promise, after 3 seconds.
In the meanwhile this completes, then()
is called on line 7 with an anonymous function as the first argument. Since it's the first argument and the promise is not yet settled, this anonymous function is queued up on successCallbackQueue
.
Finally after 3 seconds, resolve()
is called by the timeout function in line 3 and this in turn fires the function saved in successCallbackQueue
.
Thus we get "Hello"
logged!
Settled promise
Everything is extremely simple when then()
is called while the promise is settled.
Guess what happens with the callback function passed to then()
when the method fires at the time its promise is settled.
It is invoked immediately
It is simply ignored
It is queued up internally in the promise
If a promise is settled, calling then()
would simply fire the passed in argument rightaway. There's is just no single reason to first queue the callback anywhere and then execute it - it doesn't even make sense!
But one thing you gotta know here is that the callback isn't fired synchronously. Rather, it's fired asynchronously i.e it will be delayed for as long as the call stack is not free.
Consider the code below:
ByeHello
You might think that we've made a typo, in writing "Bye"
before "Hello"
in the console above; but this isn't a typo - it's the weird truth of asynchronous operations!
Because the callback passed to then()
fires asynchronously, it will wait until all synchronous tasks are completed in the entire script and the internal JavaScript engine's call stack is empty.
This is what happens in the code above.
A promise is created and immediately resolved inside the executor.
After this
then()
is called, and likewise (since its promise is settled) itsonFulfilled
callback is lined up on the task queue of the JavaScript engine.Next, a console log is made saying
"Bye"
. This completes execution of the main script i.e the call stack is now ready to execute things in the task queue.The
onFulfilled()
function enters the call stack and consequently gets executed. This results in the second log saying"Hello"
.
Thus we get the log sequence "Bye"
and then "Hello"
.
Last updated