Javascript Promises In Depth

0
254
views

Why are promises important?

Javascript is single threaded. That means when a Javascript program is run, it is executed line by line.

No two operations can occur at the same time in the language. The presence of Javascript in a browser introduces further complexity since Javascript is run in the same queue as styles, user actions and more.

In order to get around the single threaded nature of Javascript, many programmers will use event callbacks like these:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

Unfortunately, in the example above, it is possible that we started listening for the events after they’ve already happened. To get around this you’d need to write a function that checks for the presence of a page element before firing the event listener.


var img1 = document.querySelector('.img-1');

function loaded() {
  // woo yey image loaded
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // argh everything's broken
});

Unfortunately, this does not catch images that error before we have a chance to listen to them.

In addition, this code only handles for the loading of a single element – what if we want to check for multiple images?

Going Beyond Events

Events have their place. They are great for when things can happen multiple times on a single DOM element or object. But when it comes to asynchronous success or failure, you would ideally want something like this:


img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

This is in effect what Javascript Promises provide. For example, if HTML elements had a “ready” method that returned a promise, it might look something like this:


img1.ready().then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()]).then(function() {
  // all loaded
}, function() {
  // one or more failed
});

  • In essence, promises are built like event listeners except,
  • A promise can only succeed or fail once, never twice, and it can never change from success to failure. If a promise has succeeded or failed, and you later add a success or failure callback, the correct callback will be called, even though that event took place earlier
  • Promises are extremely useful for async successes or failures. That’s because you are generally less interested in the exact time something became available, and more interested in the outcome.

Promises are extremely useful for async successes or failures. That’s because you are generally less interested in the exact time something became available, and more interested in the outcome.

Important Promise Terminology

A promise can be:

  • Fulfilled The action relating to the promise succeeded
  • Rejected The action relating to the promise failed
  • Pending Hasn’t fulfilled or rejected yet
  • Settled Has been fulfilled or rejected

Promises have been around for a while in the form of libraries, such as: * Q 
when 
WinJS
RSVP.js

But now in ES6, They are finally native in Javascript. JavaScript promises share a common, standardized behavior called Promises/A+. If you’re a jQuery user, they have something similar called Deferreds. However, Deferreds aren’t Promise/A+ compliant, which makes them subtly different and less useful, so beware. jQuery also has a Promise type, but this is just a subset of Deferred and has the same issues.

JavaScript promises are similar to the implementation found in RSVP.js.

Here’s how you create a promise:


var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
}

Promise constructors take one argument, a callback with two parameters: resolve, and reject. Do something within the callback, then call resolve if everything worked, otherwise call reject.

Like “throw” in plain old JavaScript, it’s customary, but not required, to reject with an Error object. The benefit of Error objects is they capture a stack trace, making debugging tools more helpful.

Here’s how you use that promise:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

“then” takes two arguments, a callback for a success case, and another for the failure case. Both are optional, so you can add a callback for the success or failure case only.

JavaScript promises started out in the DOM as “Futures”, renamed to “Promises”, and finally moved into JavaScript. Having them in JavaScript rather than the DOM is great because they’ll be available in non-browser JS contexts such as Node.js (whether they make use of them in their core APIs is another question).

Async code simplified with Promises

Let’s dive into an example of Async code in action. In this example, we will:

  1. Start a spinner to indicate loading
  2. Fetch some JSON for a story, which gives us the title, and urls for each chapter
  3. Add title to the page
  4. Fetch each chapter
  5. Add the story to the page
  6. Stop the spinner

Let’s start by fetching data from the network let’s write a simple function to make a GET request:


function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

Promise API Reference

All methods work in * Chrome * Opera * Firefox * Microsoft Edge * Safari

Static Methods

Promise.resolve(promise);

Returns promise (only if promise.constructor == Promise)

Promise.resolve(thenable);

Make a new promise from the thenable. A thenable is promise-like in as far as it has a “then” method.

Promise.resolve(obj);

Make a promise that fulfills to obj. in this situation.

Promise.reject(obj);

Make a promise that rejects to obj. For consistency and debugging (e.g. stack traces), obj should be an instanceof Error.

Promise.all(array);

Make a promise that fulfills when every item in the array fulfills, and rejects if (and when) any item rejects. Each array item is passed to Promise.resolve, so the array can be a mixture of promise-like objects and other objects. The fulfillment value is an array (in order) of fulfillment values. The rejection value is the first rejection value.

Promise.race(array);

Make a Promise that fulfills as soon as any item fulfills, or rejects as soon as any item rejects, whichever happens first. Constructor.

new Promise(function(resolve, reject) {});
resolve(thenable)

Your promise will be fulfilled/rejected with the outcome of then able

resolve(obj)

Your promise is fulfilled with obj ~~~javascript reject(obj) ~~~

Your promise is rejected with obj. For consistency and debugging (eg stack traces), obj should be an instanceof Error. Any errors thrown in the constructor callback will be implicitly passed to reject().

Instance Methods

promise.then(onFulfilled, onRejected)

onFulfilled is called when/if “promise” resolves. onRejected is called when/if “promise” rejects. Both are optional if either/both are omitted the nextonFulfilled/onRejected in the chain is called.

Both callbacks have a single parameter, the fulfillment value or rejection reason. “then” returns a new promise equivalent to the value you return from onFulfilled/onRejected after being passed through Promise.resolve. If an error is thrown in the callback, the returned promise rejects with that error. ~~~javascript promise.catch(onRejected) ~~~

Sugar for promise.then(undefined, onRejected) So much more can be done with Promises

I hope this was helpful as a beginner primer or Promises. There is a great deal more that can be done with this ES6 feature!

LEAVE A REPLY

Please enter your comment!
Please enter your name here