A Tutorial With Examples on JavaScript Promises

Promises are a popular subject among JavaScript developers, and it’s essential to familiarize yourself with them. They can be challenging to grasp at first, requiring several tutorials, examples, and considerable practice to fully understand.

This tutorial aims to simplify JavaScript Promises and encourage their practical use. We’ll explore their definition, purpose, and mechanics. Each step is paired with a jsbin code example for hands-on learning and further experimentation.

JavaScript promises are explained in this comprehensive tutorial.

What constitutes a JavaScript promise?

A promise is a method that will eventually yield a value. Think of it as the asynchronous counterpart to a getter function. Its core concept can be summarized as:

1
2
3
promise.then(function(value) {
  // Do something with the 'value'
});

Promises offer an alternative to asynchronous callbacks with several advantages. They’re gaining traction as libraries and frameworks increasingly adopt them for asynchronous operations. Ember.js is a prime example of such a framework.

Various several libraries implement the the Promises/A+ specification. We’ll cover the fundamental terminology and illustrate the underlying concepts through practical JavaScript promises examples. The code demonstrations will utilize rsvp.js, a widely used implementation library.

Prepare yourself, we’ll be rolling plenty of dice!

Acquiring the rsvp.js library

Both server-side and client-side applications can utilize Promises and, consequently, rsvp.js. To integrate it with nodejs, navigate to your project directory and execute:

1
npm install --save rsvp

If you’re working on the front-end and employing bower, it’s merely a

1
bower install -S rsvp

away.

Alternatively, for immediate use, include it directly through a script tag (easily accomplished in jsbin via the “Add library” dropdown):

1
<script src="//cdn.jsdelivr.net/rsvp/3.0.6/rsvp.js"></script>

Analyzing the properties of a promise

A promise can exist in one of three states: pending, fulfilled, or rejected. Upon creation, it resides in the pending state, from which it can transition to either fulfilled or rejected. This transition is termed as the resolution of the promise. The resolved state marks the promise’s final state, remaining constant once achieved.

In rsvp.js, promises are generated using a mechanism known as a revealing constructor. This constructor accepts a single function as a parameter and immediately invokes it with two arguments, fulfill and reject, responsible for transitioning the promise to the fulfilled or rejected state, respectively:

1
2
3
var promise = new RSVP.Promise(function(fulfill, reject) {
  (...)
});

This JavaScript promises pattern, termed a revealing constructor, grants the constructor function visibility into the single function argument’s capabilities while preventing external entities from manipulating the promise’s state.

Promise consumers can respond to state changes by registering handlers using the then method. It accepts both a fulfillment and a rejection handler function, either of which can be omitted.

1
promise.then(onFulfilled, onRejected);

The execution of either the onFulfilled or onRejected handler occurs asynchronously, contingent on the outcome of the promise’s resolution process.

Let’s examine an example to illustrate the execution order:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function dieToss() {
  return Math.floor(Math.random() * 6) + 1;
}

console.log('1');
var promise = new RSVP.Promise(function(fulfill, reject) {
  var n = dieToss();
  if (n === 6) {
    fulfill(n);
  } else {
    reject(n);
  }
  console.log('2');
});

promise.then(function(toss) {
  console.log('Yay, threw a ' + toss + '.');  
}, function(toss) {
  console.log('Oh, noes, threw a ' + toss + '.');  
});
console.log('3');

This snippet generates output akin to the following:

1
2
3
4
1
2
3
Oh, noes, threw a 4.

However, if luck prevails, we might observe:

1
2
3
4
1
2
3
Yay, threw a 6.

This promises tutorial highlights two key points.

Firstly, the handlers attached to the promise are indeed executed asynchronously, following the completion of all preceding code.

Secondly, the fulfillment handler is invoked solely when the promise is fulfilled, receiving the resolution value (in this scenario, the dice roll result). The same principle applies to the rejection handler.

Chaining promises and the trickle-down effect

The specification mandates that the then function (handlers) must return a promise themselves. This enables promise chaining, yielding code that resembles synchronous execution:

1
2
3
4
5
signupPayingUser
  .then(displayHoorayMessage)
  .then(queueWelcomeEmail)
  .then(queueHandwrittenPostcard)
  .then(redirectToThankYouPage)

In this example, signupPayingUser returns a promise, and each function within the chain is invoked with the return value of its predecessor upon completion. Effectively, this serializes calls without obstructing the main execution thread.

To demonstrate how each promise resolves with the return value of the preceding element in the chain, we return to our dice-tossing scenario. Our objective is to roll the dice a maximum of three times, or until the first six appears jsbin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function dieToss() {
  return Math.floor(Math.random() * 6) + 1;  
}

function tossASix() {
  return new RSVP.Promise(function(fulfill, reject) {
    var n = Math.floor(Math.random() * 6) + 1;
    if (n === 6) {
      fulfill(n);
    } else {
      reject(n);
    }
  });
}

function logAndTossAgain(toss) {
  console.log("Tossed a " + toss + ", need to try again.");
  return tossASix();
}

function logSuccess(toss) {
  console.log("Yay, managed to toss a " + toss + ".");
}

function logFailure(toss) {
  console.log("Tossed a " + toss + ". Too bad, couldn't roll a six");
}

tossASix()
  .then(null, logAndTossAgain)   //Roll first time
  .then(null, logAndTossAgain)   //Roll second time
  .then(logSuccess, logFailure); //Roll third and last time

Upon executing this promises example, you’ll encounter output similar to this on the console:

1
2
3
Tossed a 2, need to try again.
Tossed a 1, need to try again.
Tossed a 4. Too bad, couldn't roll a six.

The promise returned by tossASix is rejected unless a six is rolled, triggering the rejection handler with the actual roll value. logAndTossAgain displays this result on the console and returns a promise for another dice roll. This subsequent roll, if unsuccessful, is also rejected and logged by the next logAndTossAgain.

Occasionally, fortune might favor you*, yielding a six:

1
2
Tossed a 4, need to try again.
Yay, managed to toss a 6.

* Achieving this doesn’t necessitate exceptional luck; the probability of rolling at least one six within three attempts is roughly 42%.

This example reveals an additional insight. Notice how subsequent to the first successful six, no further rolls occur. Observe that all fulfillment handlers (the first arguments of then calls) in the chain are null except for the final one, logSuccess. According to the specification, if a handler (fulfillment or rejection) is not a function, the returned promise must resolve (fulfilled or rejected) with the same value. In this instance, the fulfillment handler, being null and therefore not a function, results in the promise being fulfilled with a value of 6. Consequently, the subsequent promise returned by the then call (the next in the chain) is also fulfilled with 6.

This pattern persists until an actual fulfillment handler (a function) is encountered. In our case, this occurs at the chain’s end, where the result is printed to the console.

Error management

The Promises/A+ specification dictates that promise rejections or errors thrown within a rejection handler should be intercepted by a downstream rejection handler.

The trickle-down technique offers a clean approach to error handling:

1
2
3
4
5
6
signupPayingUser
  .then(displayHoorayMessage)
  .then(queueWelcomeEmail)
  .then(queueHandwrittenPostcard)
  .then(redirectToThankYouPage)
  .then(null, displayAndSendErrorReport)

By positioning a rejection handler at the very end of the chain, any rejection or error within a fulfillment handler will propagate downwards until it reaches displayAndSendErrorReport.

Let’s revisit our dice example and observe this in action. Suppose we aim to asynchronously roll dice and log the results:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var tossTable = {
  1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six'
};

function toss() {
  return new RSVP.Promise(function(fulfill, reject) {
    var n = Math.floor(Math.random() * 6) + 1;
    fulfill(n);
  });
}

function logAndTossAgain(toss) {
  var tossWord = tossTable[toss];
  console.log("Tossed a " + tossWord.toUppercase() + ".");
}

toss()
  .then(logAndTossAgain)
  .then(logAndTossAgain)
  .then(logAndTossAgain);

Executing this code yields no apparent output, neither console messages nor errors.

However, an error is indeed thrown behind the scenes. Its absence stems from the lack of rejection handlers in the chain. Since handler code executes asynchronously on a fresh stack, it doesn’t even reach the console log. Let’s rectify this fix this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function logAndTossAgain(toss) {
  var tossWord = tossTable[toss];
  console.log("Tossed a " + tossWord.toUpperCase() + ".");
}

function logErrorMessage(error) {
  console.log("Oops: " + error.message);
}

toss()
  .then(logAndTossAgain)
  .then(logAndTossAgain)
  .then(logAndTossAgain)
  .then(null, logErrorMessage);

Now, running the code exposes the error:

1
2
"Tossed a TWO."
"Oops: Cannot read property 'toUpperCase' of undefined"

Our oversight was neglecting to return a value from logAndTossAgain, causing the second promise to be fulfilled with undefined. Subsequently, the next fulfillment handler attempts to invoke toUpperCase on this undefined value, resulting in an error. This underscores another crucial point: always ensure a return value from handlers, or be prepared to handle the absence of a value in subsequent handlers.

Constructing higher-level promises

Having covered the basics of JavaScript promises, we can now leverage their composability to create “compound” promises tailored to specific behaviors. The rsvp.js library provides several pre-built options, and you’re always welcome to craft your own using the primitives and these higher-level constructs.

Our final, more intricate example ventures into the realm of AD&D role-playing, utilizing dice rolls to determine character scores. Such scores are typically derived from rolling three dice for each skill of the character.

Let’s examine the code first and then delve into the novel aspects:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function toss() {
  var n = Math.floor(Math.random() * 6) + 1;
  return new RSVP.resolve(n); // [1]
}

function threeDice() {
  var tosses = [];
  
  function add(x, y) {
    return x + y;
  }
  
  for (var i=0; i<3; i++) { tosses.push(toss()); }
  
  return RSVP.all(tosses).then(function(results) { // [2]
    return results.reduce(add); // [3]
  });
}

function logResults(result) {
  console.log("Rolled " + result + " with three dice.");
}

function logErrorMessage(error) {
  console.log("Oops: " + error.message);
}

threeDice()
  .then(logResults)
  .then(null, logErrorMessage);

The toss function should be familiar from our previous example. It generates a promise that consistently resolves with the result of a dice roll. Here, we’ve employed RSVP.resolve, a convenient method for creating such promises with minimal overhead (see [1] in the code).

Within threeDice, we create three promises, each representing a dice roll, and combine them using RSVP.all. This function accepts an array of promises and resolves with an array of their respective resolved values, preserving their order. Consequently, we obtain the roll results in results (see [2]) and return a promise fulfilled with their sum (see [3]).

Resolving the final promise then logs the total:

1
"Rolled 11 with three dice"

Applying promises to real-world scenarios

The applications of JavaScript promises extend far beyond gratuitously asynchronous dice rolls.

Consider replacing the three dice rolls with three AJAX requests to distinct endpoints, proceeding only upon successful completion of all requests (or handling any failures). This illustrates a practical use case for promises and RSVP.all.

Properly implemented promises result in readable code that’s easier to understand and debug compared to callbacks. They eliminate the need for conventions like error handling, as these aspects are inherently addressed by the specification.

This JavaScript tutorial merely scratches the surface of what promises can achieve. Promise libraries offer a wealth of methods and low-level constructors at your disposal. Master these tools, and unlock boundless possibilities.

Meet the author

Balint Erdi, once an avid role-playing and AD&D enthusiast, now channels his passion into promises and Ember.js. His enduring love for rock & roll inspired him to create a book on Ember.js, an application drawing upon a rock & roll theme. Stay tuned for its launch Sign up here.

Licensed under CC BY-NC-SA 4.0