JavaScript Promises - Basics

Chapter 22 35 mins

Learning outcomes:

  1. Creating a promise
  2. The then() method
  3. The executor function passed to Promise()
  4. How then() works under the hood

Introduction

In the previous chapter, JavaScript Promises — Introduction, we learnt a great of information regarding the idea behind using promises in JavaScript.

In particular, we learnt about how JavaScript naturally dealt with asynchronous operations with the help of events and callbacks; the issues with using callbacks, i.e. the 'callback hell'; the history of the evolution of promises in JavaScript; and what are promises.

Now in this chapter, our aim is to learn how to actually create a promise in JavaScript. Here, we shall explore the different concepts surrounding the creation of a rudimentary promise wrapping a basic async operation.

Deciding the async operation

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 the asynchronous operation we will be carrying out in our promise.

Recall from the previous chapter, JavaScript Promises — Introduction, that a promise is meant to simplify writing asynchronous code, so before we can use a promise, we first have to come up with an async operation.

Some quick options are to dispatch an XHR request or to set a timer using setTimeout().

The latter, as we all know, is both simple to use and understand; likewise, we'll be using it as the async operation in the following discussion.

Consider the task of showing a log to the user after 3 seconds.

To do so, we can write something like the following:

setTimeout(function() {
   console.log('Hello');
}, 3000);

The async operation here is the 'counting' of the timer, perfomed internally by the browser in the background. When it completes, i.e. when the timer ends, the callback passed to setTimeout() gets executed, consequently making a log in the console.

Note, however, that the code above is based on a callback approach. Promises are all about a better approach of working with async code and putting aside the error-prone naive callback approach.

In the following sections, we'll accomplish this same task, of showing a log after 3 seconds, albeit using a promise.

Creating a promise

The very first step in creating a promise in JavaScript, to represent an async operation, is to use the Promise() constructor.

The Promise() constructor accepts a single argument which is a function encapsulating the code for the async operation.

The reason for encapsulating the code inside a function is so that the Promise() constructor itself has control over when and how to execute the code contained in the function.

The spec calls this function the executor.

The executor function is meant to execute an asynchronous operation.

Zooming a little bit over the 'how to execute the code contained in the function' part, the Promise() constructor calls the executor with two arguments, both functions.

According to the spec, the first argument is called resolve while the second one is called reject.

The purpose of these functions is dead simple: so that the async operation can change the state of the promise to 'fulfilled' or 'rejected' once its fate has been decided.

After all, when an async operation reaches completion (due to success or maybe failure), there has to be some way to signal this from within the executor, and that's done via these argument functions.

But what exactly is meant by the state of a promise?

The states of a promise

By this point, we already know that a promise encapsulates a given async operation, and that all async operations complete (succeed or fail) at some point in the future.

Since the setup of an async operation, it can fall into one of the three cases: still being processed; succeeded; or failed.

This is exactly what the idea of a promise's state is.

Essentially, at any point in time, a promise can be in one of the three states: pending, fulfilled or rejected.

Here's what these states mean:

  • Pending means that the underlying async operation is still ongoing and no judgements can be made about its outcome yet.
  • Fulfilled means that the async operation has been succeeded.
  • Rejected means that the async operation has been failed.

A promise in the 'pending' state is sometimes also referred to as an unsettled promise. Similarly, a promise in either the 'fulfilled' or 'rejected' state is referred to as a settled promise, as the underlying async operation has literally been settled.

Every Promise object has an internal slot, [[PromiseState]], according to the spec, to reflect this attribute of the promise.

Initially, when a promise is created, its state is 'pending'.

To get the state to transition to 'fulfilled', or similarly 'rejected', we ought to invoke the executor's arguments resolve() or reject(), respectively.

Promise resolution is done using resolve() and 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.

With the state of a promise understood, the next logical concept to comprehend is the value of a promise.

Typically, when an async operation completes, either by virtue of success or failure, it is ready with some sort of data to be utilized for further actions.

For example, when an XHR request completes successfully, it is ready with the data of the response in the responseText property. However, if the request fails due to a network error, the description of the error is provided via the error event.

In promise parlance, we refer to this as the value of the promise.

The value of a promise

If the outer world knows that a promise has been resolved, then it should also know the 'result' of the underylying async operation.

The same goes for the failure case as well, i.e. if the outer world knows that a promise has been rejected, then it should also know the 'reason' for the failure of the underlying async operation.

This is where the idea of a promise's value steps in.

The value of a promise is basically a means of representing the outcome of the underlying async operation that it performs.

A promise's value is set by passing an argument to the resolve() or the reject() function in the executor.

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 to resolve it with, i.e result of the underlying async operation. This can easily be accomplished by providing an argument to resolve().

It's not strictly required to provide an argument to resolve() or reject(); it's just that when we do provide one, the promise is resolved or rejected with that value, otherwise the value is taken to be undefined.

With the idea of a promise's value understood as well, let's now create a promise to placehold the asynchronous task of setting up a timer for 3 seconds.

Consider the code below:

var timerPromise = new Promise(function(resolve, reject) {
   setTimeout(function() {
      resolve('Hello');
   }, 3000);
});

The Promise() constructor is instantiated, with the executor setting up a timer for 3 seconds.

When the timer completes, the callback passed to setTimeout() is called, thereby, invoking the resolve() parameter of the executor, with the argument 'Hello'. This fulfills the promise with the value 'Hello'.

In the end, the instantiated promise is stored in the timerPromise variable for later use.

This later use is when we invoke the then() method on the promise object in order to execute a piece of code when it gets resolved.

The then() method

The then() method of the Promise interface is used to execute a function when a promise is resolved or rejected.

The name 'then' is quite self-explanatory in what the method serves to do; that is, the then() method instructs the promise object something like: "perform that async operation and then, once it completes, do this."

then() accepts two arguments (similar in order to the ones provided to the executor):

  1. A function to call once the promise is fulfilled.
  2. A function to call once the promise is rejected.

In the ECMAScript spec, these callbacks are referred to as onFulfilled() and onRejected(), respectively.

Synactically, this could be expressed as follows:

promise.then(onFulfilled, onRejected)

To keep it very simple for now, leaving the detailed aspects of then() to be discussed later in this chapter, just try to appreciate that then() queues up callbacks on a promise object, to be fired once it get resolved or rejected.

Let's now extend the promise code we wrote above to make a console log once the timer completes:

var timerPromise = new Promise(function(resolve, reject) {
   setTimeout(function() {
      resolve('Hello');
   }, 3000);
});

timerPromise.then(function(value) {
   console.log(value);
});

Here's a live example of this code:

Live Example

Time to explain what's happening here...

In line 7, by calling then() on timerPromise, we give it an anonymous callback function to be fired once the underlying async operation completes successfully, i.e when the resolve() function is invoked in the executor.

As soon as resolve('Hello') is called in line 3, roughly after a span of 3 seconds, this anonymous callback 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 the task of logging 'Hello' to the console after 3 seconds.

And this is the whole anatomy of a promise. Simple, wasn't it?

If the promise object returned by calling the Promise() constructor isn't required to be passed around the program, and we just want to execute some code when it completes, we can directly call then() on the promise.

This is demonstrated below:

new Promise(function(resolve, reject) {
   setTimeout(function() {
      resolve('Hello');
   }, 3000);
})
.then(function(value) {
   console.log(value);
});

Take note of the . preceding then() in the code above — it instructs it to the engine that the .then() call is, in effect, a method call on the object returned by the preceding expression.

Just to recap everything:

  • First, a promise object is instantiated by calling new Promise().
  • The Promise() constructor is provided with an executor function where an async operation gets performed.
  • The operation completes at some point in the future. At this point, depending on whether it succeeded or failed, the function resolve() or reject() is called, respectively.
  • This results in the respective callback function provided to then() to be executed.

The executor function

While working with the executor function, passed into the Promise() constructor where an async operation gets perfomed, we shall know about certain technical aspects of it.

This section is devoted to understanding these technical aspects.

First things first, the executor function is executed immediately by the internal engine as soon as the Promise() constructor is called.

This can be confirmed by the code below:

console.log('1');

new Promise(function(resolve, reject) {
   console.log('2');
});

console.log('3');
1 2 3

It make the logs in chronological order: 1, 2 and then 3. Since the log 2 appears before 3, it confirms that the function passed into Promise() is executed immediately.

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 home-made delay() function:

function delay(time) {
   var d = new Date();
   while (new Date() - d < time);
}

var promise = new Promise(function(resolve, reject) {
   delay(5000); // Delay the page render for 5 seconds
   console.log("OK!")
});

Live Example

If you're on the actual document window, you'll see a blank screen for roughly 5 seconds after you run this code. Otherwise, in the developer tools on the console tab, you'll see a log after 5 seconds.

Moving on, the next thing to realize while crafting the executor is that what we previously called resolve and reject are, in effect, parameters of the executor function. Hence, we could name them in any way we like.

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).

Following, we replace the resolve and reject parameters from the previous code with succeed and fail, respectively:

var timerPromise = new Promise(function(succeed, fail) {
   setTimeout(function() {
      succeed('Hello');
   }, 3000);
});

timerPromise.then(function(msg) {
   console.log(msg); // 'Hello'
});

Since the parameters of the executor have been renamed here, now to resolve the promise, we need to call succeed. Don't make the mistake of calling resolve(); it doesn't exist in the code above!

It's simply upto us what names we want to go with. However, the names resolve and reject are quite conventional and it's much better to stick to using them.

The final thing left to discuss in this section is that if Promise() is called without an argument, or if that argument is not a function, an exception is thrown.

Two such examples are illustrated below.

Following, we invoke the Promise() constructor without any argument:

new Promise(); // No arguments
Uncaught TypeError: Promise resolver undefined is not a function

And following we invoke the constructor with a non-function argument:

new Promise(10); // A non-function argument
Uncaught TypeError: Promise resolver 10 is not a function

Precisely speaking, the previous case where we don't pass in any arguments to Promise() is basically just calling Promise() with undefined as the first argument, which is also erroneous.

How then() works under the hood?

A detailed discussion on then() would be incomplete if the resolve() and reject() functions of the executor are left untouched.

Likewise, in this section we'll understand the link between then() and resolve() and reject(), and how they interact with one another via an internal callback queue maintained by the promise.

Appreciate the fact that a promise can be in one of the two general states at the time its then() method is called: unsettled or settled.

With this in mind, try to think logically on how the method then() would work in either of these cases.

Unsettled promise

If then() is called on an unsettled promise, what do you think would happen?

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 specify that a given function shall be invoked when the promise settles. The function is provided as an argument to the then() method.

If a promise is unsettled and we invoke then() on it, there's no point of executing the callback sent to then() at this stage.

But we can't also just ignore the callback passed to then(), otherwise it would be useless to call then() prior to a promies's settlement.

The only way out is to store the given callback within the promise and fire it as soon as the promise is settled (by calling resolve() or reject() in executor).

So what's the mechanism of storing the callback?

Let's discuss it...

How are the callbacks passed to then() stored?

Every promise object internally maintains two callback queues: one holding all the functions to fire on its resolution and the other holding all the 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() while a promise is still unsettled, however, these lists fill up; the given callback arguments line up into their respective queues one after another. With the eventual settlement of the promise, these queues once again get emptied.

Let's see a detailed view of how resolve() and reject() work.

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 'fulfilled' 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.

Time to understand all of this with the help of our previous code.

Following is the code we wrote above:

var timerPromise = new Promise(function(resolve, reject) {
   setTimeout(function() {
      resolve('Hello');
   }, 3000);
});

timerPromise.then(function(value) {
   console.log(value);
});

So what's 'exactly' happening over here?

First, a new promise is created, with the executor function setting up a timer for 3 seconds.

In the meanwhile the timer completes, then() is called on line 7 with a callback as its first argument. Since it's the first argument, and the promise is not yet settled, this callback function gets queued up in the internal successCallbackQueue list.

Finally, after 3 seconds, resolve() is called by setTimeout()'s callback in line 3.

This empties and executes every function in successCallbackQueue before transitioning the promise's state to 'fulfilled' and its value to 'Hello'.

Ultimately, the callback provided to then() gets executed, and we get 'Hello' logged in the console.

Simply amazing!

Settled promise

Everything is extremely simple when then() is called on a settled promise.

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() on it would get the callback executed right away.

There's is just no single reason to first queue the callback anywhere and then execute it; this doesn't even make sense!

But one thing to take note of here is that the callback is NOT fired synchronously; rather, it's fired asynchronously, i.e it will be delayed for as long as the call stack is not free.

Let's demonstrate this using an example:

var promise = new Promise(function(resolve, reject) {
   resolve('Hello');
});

promise.then(function(value) {
   console.log(value);
});

console.log('Bye');

What do you think would this code log?

Well, here's the output produced:

Bye
Hello

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 behavior 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 call stack is empty.

This is exactly what happens in the code above.

  • A promise is created and immediately resolved inside the executor.
  • After this, then() is called with an onFulfilled() callback.
  • Because the promise is settled at this point, the given onFulfilled() callback is executed immediately albeit asynchronously, i.e the callback lines up in the task queue.
  • Next, the console.log('Bye') statement is executed. This completes execution of the main script, leaving the call stack free to execute things lined up in the task queue.
  • Our onFulfilled() callback is dequeued from the task queue and put on the call stack to be executed.
  • This results in the statement console.log('Hello') being executed.

Thus, we get the log sequence 'Bye' followed by 'Hello'.

In conclusion

And this completes our long discussion on the basics of promises.

The thing is that there are such minute details to explore in promises that even a brief discussion would cross the 1000-word limit in no time.

In this chapter we've covered some real ground on the topic of promises, but we're still not even close to the fifty percent mark.

In the next chapter, JavaScript Promises — Chaining, we shall understand one of the most stunning features of promises, i.e chaining, before moving over to consider how to effectively handling errors in promises, in the next-to-next chapter, JavaScript Promises — Error Handling.

Make sure you're confident with all the concepts discussed on this page before proceeding forward. Once you're confident that you've got them right, test out your knowledge in the JavaScript Promises — Basics Quiz.

Read, learn, test and grow!