Coding / Programming Videos

Post your favorite coding videos and share them with others!

JavaScript Promises – SuperJavascipt

Source link

Promises in JavaScript are a tool for dealing with callbacks. I’ll cover all the basics about promises here, but to understand JavaScript promises it is good to understand the motivation behind them. So we will have a look at how they compare to plain callbacks.

Plain Callbacks

Here is an example of the setTimeOut callback that I covered in this tutorial:

setTimeout(function(){
  alert('5 seconds has passed');
}, 5000);

As a quick recap, setTimeout will wait for a period of time, such as 5 seconds in the example above, and then call the function you pass in to it. This function is referred to as the callback function.

Now let’s imagine you want to wait another 4 seconds, and do something. What would that code look like? You’d have to call setTimeout again, but only after the first time out has finished. So your code would look like this:

setTimeout(function(){
  alert('5 seconds has passed');
  setTimeout(function(){
    alert('4 seconds has passed');
  }, 4000);
}, 5000);

And if you want a third call, you’d get another level of nesting:

setTimeout(function(){
  alert('5 seconds has passed');
  setTimeout(function(){
    alert('4 seconds has passed');
    setTimeout(function(){
      alert('3 seconds has passed');
    }, 3000);
  }, 4000);
}, 5000);

Nesting means functions within functions, which when formatted cause the code to be indented more and more. Above there are 3 levels of nesting, one for each setTimeout call.

I think that this getting too unwieldy now, having so many levels of nesting. Of course you could tidy this up by having multiple functions:

setTimeout(function(){
  alert('5 seconds has passed');
  doTimeout2();
}, 5000);

function doTimeout2() {
  setTimeout(function(){
    alert('4 seconds has passed');
    doTimeout3();
  }, 4000);
}

function doTimeout3() {
  setTimeout(function(){
    alert('3 seconds has passed');
  }, 3000);
}

However, while this reduces indention it means you have to sprawl your code across many functions, when this might not be the way you want to organize things. It get’s worse when dealing with error cases.

Callbacks For Errors

The setTimeout uses a single callback. However many functions have 2 (or more) callbacks. There is often one for success and another for failure. For example when using XMLHttpRequest to make a request to the server, we can set up 2 callbacks, one for load which means a successful response, and one for error which means an unexpected error:

var request = new XMLHttpRequest();

// Callback that is called on success:
request.addEventListener("load", function() {
  console.log(request.responseText);
});

// Callback that is called on failure:
request.addEventListener("error", function() {
  console.log('error');
});

// Make the request to get the data from this url:
request.open("GET", "https://httpbin.org/get");
request.send();

What if you want to call a 3 services, one after the other, and you want to handle all of the possible error cases. What would that code look like? This is what is often called “callback hell” in Javascript, creating tangled, heavily nested code that is hard to read.

There is a solution to this, and as you might have guessed it is JavaScript Promises.

JavaScript Promises Basics

Make a Promise

A promise is an object in JavaScript that “wraps” a callback function, and provides a convenient and consistent way to handle success and failure scenarios.

Here is how you can create a promise from setTimeOut:

var promise1 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve('timeout elapsed');
  }, 1000);
});

Understanding this code can be a bit tricky at first, so let’s go through it slowly.

The Promise constructor takes a function that will do some work that typically involves a callback. In this case we are using setTimeOut which will schedule some code to run a bit later.

I will zoom in on the callback function that we pass into the Promise constructor:

function(resolve, reject) {
  setTimeout(function() {
    resolve('timeout elapsed');
  }, 1000);
};

It has two arguments: resolve and reject.

resolve is a function that you call if what you have done has been successful. It takes as an argument any data that you now have as a result of the success. For example this could be a response from a server to say you have successfully logged in.

reject is a function that you call if what you have done has failed. It also can take an argument with some data about why it failed.

In the example above, we call resolve in the callback of setTimeout. This means that once 1 second has passed, resolve is called to indicate to the promise that we are finished. We pass in the string 'timeout elapsed' which can be used by whatever handles the promise.

In this simple case there is no need to call reject as there is no failure scenario.

Use a Promise

With the promise created above, we can then add a handler for when the callback is complete. We do this by calling the then method of the promise. The method takes an argument that is the callback function, as shown below:

var promise1 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve('timeout elapsed');
  }, 1000);
});

// Using the promise:
promise1.then(function(result) {
  console.log(result);
});

The code above will wait for 1 second, and then:

  1. The setTimeout will call it’s call back which resolves the promise.
  2. This causes the then callback to be called, which will log the resolve parameter, which is 'timeout elapsed', to the console.

Chaining JavaScript Promises

The then method itself returns a promise, which will resolve once the original promise has resolve and it’s then function has resolved.

This means you can chain another action to happen after the first one, like so:

// Using the promise:
promise1.then(function(result) {
  console.log(result);
}).then(function(result) {
  console.log('second action');
});

The second action will happen immediately after the first when chained in this way.

Chaining in this way is synchronous, in other words the next action happens immediately after the first one.

Chaining a Promise with another Promise

What if you have two actions that have a call back, and you want to run the first, and once that comes back run the second?

An example of this is logging into a bank, then once logged in fetching your bank balance.

Another example is the one we encountered earlier with this tutorial:

setTimeout(function(){
  alert('5 seconds has passed');
  setTimeout(function(){
    alert('4 seconds has passed');
  }, 4000);
}, 5000);

Promises can do this too. To do this you return a new promise in the then function. When you do this:

  1. The new promise is started once the first promise successfully completes.
  2. It’s callback is handled by the next then function in the chain.

Ok, I think an example will make it clearer. The code for waiting 4 seconds then 5 seconds can be written like this:

// Function to make a promise that waits for a number
// of seconds, then resolves:
function makePromise(seconds) {
  return new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve(seconds);
  }, 1000 * seconds);
});
}

makePromise(5).then(function (){
  // This gets run after 5 seconds:
  console.log('5 seconds has passed');

  // Kicks of a 4 second timer, and the next
  // `then` handles the reponse to this new
  // promise:
  return makePromise(4);
}).then(function () {
  // This gets run 4 secods after the first 5
  // seconds has elapsed:
  console.log('4 seconds has passed');
})

Chaining in this way is asynchronous, in other words the second callback will happen some time later, and other JavaScript code might be run inbetween.

Promises Are Reusable

That previous example seems like a lot of code, but we can fix that.

The beauty of promises is the makePromise code above can be put into a separate library file, because it is a general purpose promise maker for setTimeout.

To show this I am going to chain 10 timeouts together, assuming makePromise is in a library. Notice how there is no nesting like we saw earlier with the callbacks:

// We are doing the same action a few times so I have
// put it in a handy function:
function action() {
  console.log('Callback occurred');
  return makePromise(1);
}

// Wait a second then peform the action 10 times in a row,
// but each time only happening after the previous promise is
// resolved.
makePromise(1)
	.then(action)
	.then(action)
	.then(action)
	.then(action)
	.then(action)
	.then(action)
	.then(action)
	.then(action)
	.then(action)
	.then(action);

The code above will put the words 'Callback occurred' to the console output ten times, with a one second gap before each time.

Rejected promises

Some actions, such as sending a message to the server, can sometimes fail. For example if the network is disconnected. Promises provide a way to handle those failures.

Earlier I showed this example of using callbacks to handle both success and errors when making a web request:

var request = new XMLHttpRequest();

// Callback that is called on success:
request.addEventListener("load", function() {
  console.log(request.responseText);
});

// Callback that is called on failure:
request.addEventListener("error", function() {
  console.log('error');
});

// Make the request to get the data from this url:
request.open("GET", "https://httpbin.org/get");
request.send();

Creating a promise that rejects

We can convert this into a promise, by observing that we have one callback that deals with success and another that deals with errors:

var promise = new Promise(function(resolve, reject) {
  var request = new XMLHttpRequest();

  // Callback that is called on success:
  request.addEventListener("load", function(result) {
    resolve(request, result);
  });

  // Callback that is called on failure:
  request.addEventListener("error", function(result) {
    reject(request, result);
  });

  // Make the request to get the data from this url:
  request.open("GET", "https://httpbin.org/get");
  request.send();
});

promise.then(function(request){
  console.log(request.responseText);
});

It’s really the same code wrapped in a promise. The promise function doesn’t usually handle ‘doing’ things like writing to the console log, this is done instead by the user of the promise. This makes the promise reusable for different situations.

In the example above we don’t handle the failure case, but it is easy to do so. Instead of calling then we can call catch:

promise.then(function(request){
  console.log(request.responseText);
}).catch(function(request){
  console.log('An error occurred');
});

Exception handling in promises

Promises will handle any exceptions thrown in the promise function, and this will also cause the catch method to be run, with the argument being the error that was caught, for example:

var promise = new Promise(function(resolve, reject) {
  throw 'Just an error';
});

promise.then(function(){
  console.log('Success');
}).catch(function(ex){
  console.log('An error occurred: ' + ex);
});

// Result: An error occurred: Just an error

Summary

This tutorial covers JavaScript promises, that provide a neat and consistent way to handle callbacks. They can be chained together and allow you to provide handlers for both success and error outcomes.

See also:

Source link

Bookmark(0)
 

Leave a Reply

Please Login to comment
  Subscribe  
Notify of
Translate »