top

JavaScript- Moving From Callbacks to Promises and Async/Await

JavaScript is an asynchronous language by default. This means that it will not wait for certain actions to complete before it moves on to evaluate the next line of code. That’s pretty awesome because it doesn’t block the User Interface while something like an HTTP request is being made.It’s also kind of a nightmare. When all you want is for the code to execute in the order you wrote it, JavaScript can bring even the most experienced developer to their knees.In this article, we’re going to look at the myriad of different ways that JavaScript developers can handle asynchronous operations without losing their minds. So, let’s begin at the beginning. And at the beginning is something that JavaScript developers refer to affectionately as a “callback”.CallbacksOne of the things that make JavaScript fun and interesting is that it is functional in nature. This means that it is common to see functions passed around as parameters in JavaScript. The most simple example of this is the setTimeout function.setTimeout(function () {   console.log('Done!'); }, 1000); console.log('Me first'); That first parameter — the function — is the “callback”. It is executed whenever setTimeout is done and JavaScript does not wait to execute other lines of code. JavaScript has zero patience, and patience is a virtue.That’s a contrived example and you probably aren’t doing a ton of timeout calls. We see asynchronous execution more often in the real-world something like an HTTP Request.HTTP requests in JavaScript are asynchronous. The default XMLHTTP object in the browser is kind of verbose (like Charles Dickens), so let’s look at it using the request npm package. const request = require('request'); request('https://jsonplaceholder.typicode.com/posts/1', function myCallback(error, response, body)   console.log(body); });                                                      This API returns lorum ipsum text in the form of a fake blog postThe request API takes a URL and then calls a function when the HTTP request completes. In the above case, we are specifying that function (callback) inline. We could pull it out so that it is a named function. This makes our request call a little cleaner.const request = require('request'); function myCallback(error, response, body) {   console.log(body); } request('https://jsonplaceholder.typicode.com/posts/1', myCallback); Notice that there is an error parameter. If that parameter has a value, we have an error and need to handle that error. We can do that by specifying another function specifically to handle the error. const request = require('request'); function handleError(error) {   console.log(`Error occured: ${error}`); } function myCallback(error, response, body) {   if (error) handleError(error);   console.log(body); } request('https://jsonplaceholder.typicode.com/posts/1', myCallback); As is, this isn’t so bad. Where this starts to go downhill is when we have a lot of nested callbacks. Imagine a scenario where we need to get a comment, to get a post id, to get a user id, to get a user object.const request = require('request'); function handleError(error) {   console.log(`Error occured: ${error}`); } const baseURL = 'https://jsonplaceholder.typicode.com/'; request(`${baseURL}comments/53`,   function (err, res, body) {     if (err) handleError(err);     let comment = JSON.parse(body);     request(`${baseURL}posts/${comment.postId}`, function (err, res, body) {       if (err) handleError(err);       let post = JSON.parse(body);       request(`${baseURL}users/${post.userId}`, function (err, res, body) {         if (err) handleError(err);         console.log(body);       });     });   }); GROSS. That is hard on the old eyeballs. This example is particularly hard to read because the URL is almost identical in all 3 calls. The same variables repeat over and over again and each time their scope/value is different.This kind of code will make a person question their life choices. This is why we sometimes refer to this as “Callback Hell”. Not “Callback Purgatory”. Not “Callback Detention”. Callback Hell.To save all of our souls from the above code path inevitability, the JavaScript gods reached down and upgraded to promises and async/await from callback.PromisesPromises arrived some years ago in the form the of the Promises/A+ specification. We used to implement them with libraries such as Q and RSVP, but as of ES6, promises are now native to JavaScript. You’ll find them virtually anywhere that you find good ES6 support. This would include things like the Angular CLI, create-react-app, the Vue CLI and full support in Node as of 6.14.3.Promises return an object which will contain the eventual result of an asynchronous operation. To get at that “eventual result”, developers call the .then method on the promise object. Let’s back up to our original setTimeout example for a moment. How would it look if we move from callback to promises?let p = new Promise(function(resolve, reject) {   setTimeout(function() {     resolve(`Done!`);   }, 1000); }); console.log('Me First'); A new promise is created by taking in a function which has the resolve and reject functions. When you are done with whatever you need to do in your asynchronous task, you call resolve and pass the data you need. If an error is thrown, you call the reject function.Promises are usually written using lambdas  or arrow functions. This is a shorter way to compose functions without having to type out the word function. This example here is functionally equivalent to the above…let p = new Promise(function(resolve, reject) {   setTimeout(function() {     resolve(`Done!`);   }, 1000); }); console.log('Me First'); So, what do we get back here? What is the value of p? It’s a promise object.What the heck do we do with this p thing and what does “pending” even mean? Pending just means that you haven’t done anything with the result yet. You haven’t executed any code to handle the return value of this object. To handle the return of a Promise, you need to call the then method on it.Now, we can control the flow of the code by moving our second console statement inside the promise. let p = new Promise((resolve, reject) => {   setTimeout(() => {     resolve('Done!');   }, 1000); }); p.then(message => {   console.log(message);   console.log('Me First'); });OK! That’s a simple promise we’re rocking and it’s looking pretty good. Now you may think to yourself, “Geez. I’m still passing functions around and now I feel like I’m writing even more code.”And you would be right. But the point of promises isn’t “less code” per-say — it’s “more readable code”. Let’s do something more real to see how this plays out.Rehashing the HTTP example above, but this time with a promise instead of a callback.const request = require('request'); const baseURL = 'https://jsonplaceholder.typicode.com/posts/'; let getPosts = new Promise((resolve, reject) => {   request(`${baseURL}1`, (err, res, body) => {     if (err) reject(err);     resolve(body);   }); }); getPosts   .then(body => {     console.log(body);   })   .catch(error => {     console.log(`Error occured: ${error}`);   }); The flow of this code is easier to understand because the callback stays with the promise and we’re not going several levels deep. When you move between functions and levels, the brain has to keep up with variables and scope and that becomes like herding gnats at some point.Notice that if there is an err value returned by request, I am calling reject. This means that we can handle any errors in this promise object later by calling catch(), which I do at the very end. Now, what’s really nice is that most HTTP libraries these days already support a promise API. That means that you don’t need to wrap them. You can just call then and catch. In the case of a request, we need to install the request-promise-native package.Look at how nicely built-in promises simplify our code…const request = require('request-promise-native'); const baseURL = 'https://jsonplaceholder.typicode.com/posts/'; let getPosts = request(`${baseURL}1`); getPosts   .then(body => {     console.log(body);   })   .catch(error => {     console.log(`Error occured: ${error}`);   }); Really, we don’t even need to assign to an object before we resolve the promise. We can simplify this further by doing it all inline.const request = require('request-promise-native'); const baseURL = 'https://jsonplaceholder.typicode.com/posts/'; request(`${baseURL}1`)   .then(body => {     console.log(body);   })   .catch(error => {     console.log(`Error occured: ${error}`);   });Now our code is looking TIGHT. It’s easy to read and it might indeed be less code, but no promises there. Get it? HAHAHAHA…No? You’re right. That pun was DOA.Now, let’s put our promises to the real test and see what happens when we nest three of these HTTP calls like we did before. const request = require('request-promise-native'); const baseURL = 'https://jsonplaceholder.typicode.com/'; let handleError = (error) => {   console.log(`Error occured: ${error}`); }; request(`${baseURL}comments/53`)   .then(body => {     let comment = JSON.parse(body);     request(`${baseURL}posts/${comment.postId}`)       .then(body => {         let post = JSON.parse(body);         request(`${baseURL}users/${post.userId}`)           .then(body => {             console.log(body);           })           .catch(error => {             handleError(error);           });       })       .catch(error => {         handleError(error);       });   })   .catch(error => {     handleError(error);   });Good grief! That might actually be worse than the callback example. Actually, it is definitely worse. Way, way worse.Why is it worse? Aren’t promises supposed to save us from callbacks?Yes, but the lesson here is that promises can also give birth to code monsters that make you afraid to go into your source control after dark. In all seriousness, though, you will hit this scenario where you need to nest promises so let’s look at what to do about it without creating a world-class rat’s nest.First off, as a general rule of thumb, do not ever nest promises. When you do that, you are simply creating a different kind of callback hell. But it’s still callback hell. It’s just a socially acceptable one because you used promises to do it. Since you can’t nest promises, what you are going to do instead is use Promise.all to resolve everything once, after it is all finished.const request = require('request-promise-native'); const baseURL = 'https://jsonplaceholder.typicode.com/'; let handleError = (error) => {   console.log(`Error occured: ${error}`); }; request(`${baseURL}comments/53`)   .then(body => {     let comment = JSON.parse(body);     return Promise.all([body, request(`${baseURL}posts/${comment.postId}`)]);   })   .then(results => {     let post = JSON.parse(results[1]);     return Promise.all([...results, request(`${baseURL}users/${post.userId}`)]);   })   .then(results => {     console.log(results[2]);   })   .catch(error => {     handleError(error);   }); This is better and easier to read, but not because there is less code. It’s because we aren’t going several levels deep so it’s easier for your mind to keep up with the logic of the code.What we’re doing here is calling Promise.all each time we need to return a result, passing in the result(s) from the previous operation and the next Promise we want to execute. We can keep chaining then statements until we get to the end of what we need to do. The single catch at the end will handle any errors thrown on the chain.In this way, you can nest promises without nesting them. This is particularly useful when one promise depends on the result of another — and so on and so forth. These are the types of real-world scenarios you will find yourself in and hopefully you will remember this article and my terrible puns.Promises are awesome. They are; I do believe that. I also kind of believe this…Promises are essentially just a way for us to restructure callbacks so that they aren’t so soupy when we use a lot of them. But, we are still passing functions around. I always have this latent feeling that I’m not actually getting away from callbacks at all when I use Promises. Like, they’re still there but we’ve both agreed to pretend like they aren’t.There is a better way, and it’s called async/await.Async/AwaitWhen working with asynchronous code, what we actually want is for a line of code to execute before the next one does. That’s it! That’s all we’re after. All of these code gymnastics we’ve been doing is to get JavaScript to just do that. Async/Await does just that.Async/Await simplifies the way you work with promises so that your code doesn’t contain all the callbacks. That’s the first thing to understand about Async/Await . It is not a replacement for promises, it works with promises.Let’s take a look at how we can use Async/Await with our HTTP example. Let’s start with using it to make one HTTP call.const request = require('request-promise-native'); const baseURL = 'https://jsonplaceholder.typicode.com/'; async function main() {   let response = await request(`${baseURL}posts/1`);   console.log(response); } main(); There are some new terminology and structure here. Let’s break it down now…We use await to call the request object which returns a promise.Await will give us the “then” result of that promise and stick it in the response variable.Await has to be called inside a function marked “Async”. That’s why I wrapped it in async function main().The line console.log('All Done') doesn’t execute until the URL request comes back. This code is now synchronous.Pretty neat! Now, we can force JavaScript to do exactly what we want, which is to just wait until an operation finishes before moving to the next line of code. And how do we handle errors? With a proper try/catch block.const request = require('request-promise-native'); const baseURL = 'https://jsonplaceholder.typicode.com/'; async function main() {   try {     let response = await request(`${baseURL}posts/1`);     console.log(response);   } catch (error) {     console.log(error);   } } main(); We’re starting to look like real programmers here. We’ve got the async code, error handling. All we need now is that job offer from Microsoft or Google or even LEGO. I would take the LEGO job.Where this really shines is in the complex. Look at how much nicer our nested example looks now…const request = require('request-promise-native'); const baseURL = 'https://jsonplaceholder.typicode.com/'; async function main() {   try {     let result = await request(`${baseURL}comments/53`);     let comment = JSON.parse(result);         result = await request(`${baseURL}posts/${comment.postId}`);     let post = JSON.parse(result);         result = await request(`${baseURL}users/${post.userId}`);     console.log(result);   } catch (error) {     console.log(error);   } } main(); This code is much easier on the brain. There are no structures to jump through or weird paths to try and follow like Alice in Wonderland. We could simplify it even further by using a nice concise HTTP library like axios. const axios = require('axios'); const baseURL = 'https://jsonplaceholder.typicode.com/'; async function main() {   try {     let comment = await axios(`${baseURL}comments/53`);         let post = await axios(`${baseURL}posts/${comment.data.postId}`);         let user = await axios(`${baseURL}users/${post.data.userId}`);         console.log(user.data);   } catch (error) {     console.log(error);   } } main(); You can use async/await to turn to call any method that returns a promise into a synchronous line of code. If you want to make something synchronous, just wrap it in a promise and call it with async/await.Async/Await is magical! But there are a few “gotcha!”s. There is one that I want to cover specifically because there is a 100% chance you will hit it. And if I don’t prep you, I’m afraid you might get super frustrated and smash your kid’s Lego castle resulting in years of therapy for both of you.To avoid that, let’s take a look at a place where this can go wrong.Async and the forEach “gotcha!”Look at this example. Here we are going to make an async call for some data, loop over it and make an async all inside of the loop for more data. What could go wrong?const axios = require('axios'); const baseURL = 'https://jsonplaceholder.typicode.com'; async function main() {   // get all posts   let posts = await axios(`${baseURL}/posts`);   posts.data.forEach(async post => {     // get the user for this post     let user = await axios(`${baseURL}/users/${post.userId}`);     console.log(JSON.stringify(user.data.username));   });   console.log('All Done'); } main(); If you were to run this, this is what you would see… Wait. What? Aren’t forEach loops supposed to be synchronous? Why is “All Done” showing up first? That should have been last. This isn’t synchronous at all.Well, forEach is synchronous by default. But the second you put that async on the function that is executed in the loop, you just flipped the switch and made it asynchronous. That’s why the line outside the loop fires first.This can lead to problems that are super hard to debug, not to mention this is not at all what we want. So what do we do here?To handle this, we are going to use the for of loop, instead of the built-in forEach on the array. const axios = require('axios'); const baseURL = 'https://jsonplaceholder.typicode.com'; async function main() {   // get all posts   let posts = await axios(`${baseURL}/posts`);   for (let post of posts.data) {     // get the user for the post     let user = await axios(`${baseURL}/users/${post.userId}`);     console.log(JSON.stringify(user.data.username));   }   console.log('All Done'); } main(); Now that is what we wanted to begin with. I got bit by this at least half a dozen times before I was able to figure out what was happening, so I wanted to spare you the pain I’ve already endured. The Best Of Both WorldsQuite possibly my favorite feature of JavaScript is its asynchronous nature. It makes the language so much fun to work with because you can execute things without blocking the user’s interaction. It’s also one of my least favorite things because when you don’t need it, async issues can turn your code into a jacked up Rubix Cube.                                                       “Oh When” by Peanut Dela Cruz It’s fine if you know how to solve a Rubix Cube, but if you don’t, may God have mercy on your soul.Fortunately, async/await and promises are here to absolve you of your sins so you don’t ever have to visit callback hell. Bless you, my child. To understand  how to handle JavaScripts with async/await and promises more clearly you can try these examples from Github. 
Rated 4.5/5 based on 11 customer reviews
Normal Mode Dark Mode

JavaScript- Moving From Callbacks to Promises and Async/Await

Burke Holland
Blog
30th Aug, 2018
JavaScript- Moving From Callbacks to Promises and Async/Await

JavaScript is an asynchronous language by default. This means that it will not wait for certain actions to complete before it moves on to evaluate the next line of code. That’s pretty awesome because it doesn’t block the User Interface while something like an HTTP request is being made.

It’s also kind of a nightmare. When all you want is for the code to execute in the order you wrote it, JavaScript can bring even the most experienced developer to their knees.

In this article, we’re going to look at the myriad of different ways that JavaScript developers can handle asynchronous operations without losing their minds. So, let’s begin at the beginning. And at the beginning is something that JavaScript developers refer to affectionately as a “callback”.


Callbacks

One of the things that make JavaScript fun and interesting is that it is functional in nature. This means that it is common to see functions passed around as parameters in JavaScript. The most simple example of this is the setTimeout function.

setTimeout(function () {
  console.log('Done!');
}, 1000);
console.log('Me first');


Callbacks

That first parameter — the function — is the “callback”. It is executed whenever setTimeout is done and JavaScript does not wait to execute other lines of code. JavaScript has zero patience, and patience is a virtue.

That’s a contrived example and you probably aren’t doing a ton of timeout calls. We see asynchronous execution more often in the real-world something like an HTTP Request.

HTTP requests in JavaScript are asynchronous. The default XMLHTTP object in the browser is kind of verbose (like Charles Dickens), so let’s look at it using the request npm package.

 

const request = require('request');
request('https://jsonplaceholder.typicode.com/posts/1', function myCallback(error, response, body) 
  console.log(body);
});


This API returns lorum ipsum

                                                     This API returns lorum ipsum text in the form of a fake blog post

The request API takes a URL and then calls a function when the HTTP request completes. In the above case, we are specifying that function (callback) inline. We could pull it out so that it is a named function. This makes our request call a little cleaner.

const request = require('request');

function myCallback(error, response, body) {
  console.log(body);
}

request('https://jsonplaceholder.typicode.com/posts/1', myCallback);


Notice that there is an error parameter. If that parameter has a value, we have an error and need to handle that error. We can do that by specifying another function specifically to handle the error. 

const request = require('request');

function handleError(error) {
  console.log(`Error occured: ${error}`);
}

function myCallback(error, response, body) {
  if (error) handleError(error);
  console.log(body);
}

request('https://jsonplaceholder.typicode.com/posts/1', myCallback);


As is, this isn’t so bad. Where this starts to go downhill is when we have a lot of nested callbacks. Imagine a scenario where we need to get a comment, to get a post id, to get a user id, to get a user object.

const request = require('request');

function handleError(error) {
  console.log(`Error occured: ${error}`);
}

const baseURL = 'https://jsonplaceholder.typicode.com/';
request(`${baseURL}comments/53`,
  function (err, res, body) {
    if (err) handleError(err);
    let comment = JSON.parse(body);
    request(`${baseURL}posts/${comment.postId}`, function (err, res, body) {
      if (err) handleError(err);
      let post = JSON.parse(body);
      request(`${baseURL}users/${post.userId}`, function (err, res, body) {
        if (err) handleError(err);
        console.log(body);
      });
    });
  });


GROSS. That is hard on the old eyeballs. This example is particularly hard to read because the URL is almost identical in all 3 calls. The same variables repeat over and over again and each time their scope/value is different.

This kind of code will make a person question their life choices. This is why we sometimes refer to this as “Callback Hell”. Not “Callback Purgatory”. Not “Callback Detention”. Callback Hell.

To save all of our souls from the above code path inevitability, the JavaScript gods reached down and upgraded to promises and async/await from callback.


Promises

Promises arrived some years ago in the form the of the Promises/A+ specification. We used to implement them with libraries such as Q and RSVP, but as of ES6, promises are now native to JavaScript. You’ll find them virtually anywhere that you find good ES6 support. This would include things like the Angular CLI, create-react-app, the Vue CLI and full support in Node as of 6.14.3.

Promises return an object which will contain the eventual result of an asynchronous operation. To get at that “eventual result”, developers call the .then method on the promise object. 

Let’s back up to our original setTimeout example for a moment. How would it look if we move from callback to promises?

let p = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve(`Done!`);
  }, 1000);
});

console.log('Me First');


A new promise is created by taking in a function which has the resolve and reject functions. When you are done with whatever you need to do in your asynchronous task, you call resolve and pass the data you need. If an error is thrown, you call the reject function.

Promises are usually written using lambdas  or arrow functions. This is a shorter way to compose functions without having to type out the word function. This example here is functionally equivalent to the above…

let p = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve(`Done!`);
  }, 1000);
});

console.log('Me First');


So, what do we get back here? What is the value of p? It’s a promise object.


Promises

What the heck do we do with this thing and what does “pending” even mean? Pending just means that you haven’t done anything with the result yet. You haven’t executed any code to handle the return value of this object. 

To handle the return of a Promise, you need to call the then method on it.

Now, we can control the flow of the code by moving our second console statement inside the promise.

 

let p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Done!');
  }, 1000);
});

p.then(message => {
  console.log(message);
  console.log('Me First');
});


Terminal

OK! That’s a simple promise we’re rocking and it’s looking pretty good. Now you may think to yourself, “Geez. I’m still passing functions around and now I feel like I’m writing even more code.”

And you would be right. But the point of promises isn’t “less code” per-say — it’s “more readable code”. Let’s do something more real to see how this plays out.

Rehashing the HTTP example above, but this time with a promise instead of a callback.

const request = require('request');

const baseURL = 'https://jsonplaceholder.typicode.com/posts/';

let getPosts = new Promise((resolve, reject) => {
  request(`${baseURL}1`, (err, res, body) => {
    if (err) reject(err);
    resolve(body);
  });
});

getPosts
  .then(body => {
    console.log(body);
  })
  .catch(error => {
    console.log(`Error occured: ${error}`);
  });



The flow of this code is easier to understand because the callback stays with the promise and we’re not going several levels deep. When you move between functions and levels, the brain has to keep up with variables and scope and that becomes like herding gnats at some point.

Notice that if there is an err value returned by request, I am calling reject. This means that we can handle any errors in this promise object later by calling catch(), which I do at the very end. 

Now, what’s really nice is that most HTTP libraries these days already support a promise API. That means that you don’t need to wrap them. You can just call then and catch. In the case of a request, we need to install the request-promise-native package.

Look at how nicely built-in promises simplify our code…

const request = require('request-promise-native');

const baseURL = 'https://jsonplaceholder.typicode.com/posts/';

let getPosts = request(`${baseURL}1`);
getPosts
  .then(body => {
    console.log(body);
  })
  .catch(error => {
    console.log(`Error occured: ${error}`);
  });


Really, we don’t even need to assign to an object before we resolve the promise. We can simplify this further by doing it all inline.

const request = require('request-promise-native');

const baseURL = 'https://jsonplaceholder.typicode.com/posts/';

request(`${baseURL}1`)
  .then(body => {
    console.log(body);
  })
  .catch(error => {
    console.log(`Error occured: ${error}`);
  });


Now our code is looking TIGHT. It’s easy to read and it might indeed be less code, but no promises there. Get it? HAHAHAHA…No? You’re right. That pun was DOA.

Now, let’s put our promises to the real test and see what happens when we nest three of these HTTP calls like we did before. 


const request = require('request-promise-native');

const baseURL = 'https://jsonplaceholder.typicode.com/';

let handleError = (error) => {
  console.log(`Error occured: ${error}`);
};

request(`${baseURL}comments/53`)
  .then(body => {
    let comment = JSON.parse(body);
    request(`${baseURL}posts/${comment.postId}`)
      .then(body => {
        let post = JSON.parse(body);
        request(`${baseURL}users/${post.userId}`)
          .then(body => {
            console.log(body);
          })
          .catch(error => {
            handleError(error);
          });
      })
      .catch(error => {
        handleError(error);
      });
  })
  .catch(error => {
    handleError(error);
  });


Good grief! That might actually be worse than the callback example. Actually, it is definitely worse. Way, way worse.

Why is it worse? Aren’t promises supposed to save us from callbacks?

Yes, but the lesson here is that promises can also give birth to code monsters that make you afraid to go into your source control after dark. 

In all seriousness, though, you will hit this scenario where you need to nest promises so let’s look at what to do about it without creating a world-class rat’s nest.

First off, as a general rule of thumb, do not ever nest promises. When you do that, you are simply creating a different kind of callback hell. But it’s still callback hell. It’s just a socially acceptable one because you used promises to do it. 

Since you can’t nest promises, what you are going to do instead is use Promise.all to resolve everything once, after it is all finished.

const request = require('request-promise-native');

const baseURL = 'https://jsonplaceholder.typicode.com/';

let handleError = (error) => {
  console.log(`Error occured: ${error}`);
};

request(`${baseURL}comments/53`)
  .then(body => {
    let comment = JSON.parse(body);
    return Promise.all([body, request(`${baseURL}posts/${comment.postId}`)]);
  })
  .then(results => {
    let post = JSON.parse(results[1]);
    return Promise.all([...results, request(`${baseURL}users/${post.userId}`)]);
  })
  .then(results => {
    console.log(results[2]);
  })
  .catch(error => {
    handleError(error);
  });


This is better and easier to read, but not because there is less code. It’s because we aren’t going several levels deep so it’s easier for your mind to keep up with the logic of the code.

What we’re doing here is calling Promise.all each time we need to return a result, passing in the result(s) from the previous operation and the next Promise we want to execute. We can keep chaining then statements until we get to the end of what we need to do. The single catch at the end will handle any errors thrown on the chain.

In this way, you can nest promises without nesting them. This is particularly useful when one promise depends on the result of another — and so on and so forth. These are the types of real-world scenarios you will find yourself in and hopefully you will remember this article and my terrible puns.

Promises are awesome. They are; I do believe that. I also kind of believe this…

Promises are just socially acceptable callbacks

Promises are essentially just a way for us to restructure callbacks so that they aren’t so soupy when we use a lot of them. But, we are still passing functions around. I always have this latent feeling that I’m not actually getting away from callbacks at all when I use Promises. Like, they’re still there but we’ve both agreed to pretend like they aren’t.

There is a better way, and it’s called async/await.


Async/Await

When working with asynchronous code, what we actually want is for a line of code to execute before the next one does. That’s it! That’s all we’re after. All of these code gymnastics we’ve been doing is to get JavaScript to just do that. 

Async/Await does just that.

Async/Await simplifies the way you work with promises so that your code doesn’t contain all the callbacks. That’s the first thing to understand about Async/Await . It is not a replacement for promises, it works with promises.

Let’s take a look at how we can use Async/Await with our HTTP example. Let’s start with using it to make one HTTP call.

const request = require('request-promise-native');

const baseURL = 'https://jsonplaceholder.typicode.com/';

async function main() {
  let response = await request(`${baseURL}posts/1`);
  console.log(response);
}

main();

Async/Await


There are some new terminology and structure here. Let’s break it down now…

  • We use await to call the request object which returns a promise
    .
  • Await will give us the “then” result of that promise and stick it in the response variable.

  • Await has to be called inside a function marked “Async”. That’s why I wrapped it in async function main().

  • The line console.log('All Done') doesn’t execute until the URL request comes back. This code is now synchronous.

Pretty neat! Now, we can force JavaScript to do exactly what we want, which is to just wait until an operation finishes before moving to the next line of code. 

And how do we handle errors? With a proper try/catch block.

const request = require('request-promise-native');

const baseURL = 'https://jsonplaceholder.typicode.com/';

async function main() {
  try {
    let response = await request(`${baseURL}posts/1`);
    console.log(response);
  } catch (error) {
    console.log(error);
  }
}

main();


We’re starting to look like real programmers here. We’ve got the async code, error handling. All we need now is that job offer from Microsoft or Google or even LEGO. I would take the LEGO job.

Where this really shines is in the complex. Look at how much nicer our nested example looks now…

const request = require('request-promise-native');

const baseURL = 'https://jsonplaceholder.typicode.com/';

async function main() {
  try {
    let result = await request(`${baseURL}comments/53`);
    let comment = JSON.parse(result);
   
    result = await request(`${baseURL}posts/${comment.postId}`);
    let post = JSON.parse(result);
   
    result = await request(`${baseURL}users/${post.userId}`);
    console.log(result);
  } catch (error) {
    console.log(error);
  }
}

main();

This code is much easier on the brain. There are no structures to jump through or weird paths to try and follow like Alice in Wonderland. We could simplify it even further by using a nice concise HTTP library like axios. 

const axios = require('axios');

const baseURL = 'https://jsonplaceholder.typicode.com/';

async function main() {
  try {
    let comment = await axios(`${baseURL}comments/53`);
   
    let post = await axios(`${baseURL}posts/${comment.data.postId}`);
   
    let user = await axios(`${baseURL}users/${post.data.userId}`);
   
    console.log(user.data);
  } catch (error) {
    console.log(error);
  }
}

main();


You can use async/await to turn to call any method that returns a promise into a synchronous line of code. If you want to make something synchronous, just wrap it in a promise and call it with async/await.

Async/Await is magical! But there are a few “gotcha!”s. There is one that I want to cover specifically because there is a 100% chance you will hit it. And if I don’t prep you, I’m afraid you might get super frustrated and smash your kid’s Lego castle resulting in years of therapy for both of you.

To avoid that, let’s take a look at a place where this can go wrong.


Async and the forEach “gotcha!”

Look at this example. Here we are going to make an async call for some data, loop over it and make an async all inside of the loop for more data. What could go wrong?

const axios = require('axios');

const baseURL = 'https://jsonplaceholder.typicode.com';

async function main() {
  // get all posts
  let posts = await axios(`${baseURL}/posts`);

  posts.data.forEach(async post => {
    // get the user for this post
    let user = await axios(`${baseURL}/users/${post.userId}`);
    console.log(JSON.stringify(user.data.username));
  });

  console.log('All Done');
}

main();


If you were to run this, this is what you would see…

 



Async and the forEach “gotcha!”


Wait. What? Aren’t forEach loops supposed to be synchronous? Why is “All Done” showing up first? That should have been last. This isn’t synchronous at all.

Well, forEach is synchronous by default. But the second you put that async on the function that is executed in the loop, you just flipped the switch and made it asynchronous. That’s why the line outside the loop fires first.

This can lead to problems that are super hard to debug, not to mention this is not at all what we want. So what do we do here?

To handle this, we are going to use the for of loop, instead of the built-in forEach on the array.

 

const axios = require('axios');

const baseURL = 'https://jsonplaceholder.typicode.com';

async function main() {
  // get all posts
  let posts = await axios(`${baseURL}/posts`);

  for (let post of posts.data) {
    // get the user for the post
    let user = await axios(`${baseURL}/users/${post.userId}`);
    console.log(JSON.stringify(user.data.username));
  }

  console.log('All Done');
}

main();



Now that is what we wanted to begin with. I got bit by this at least half a dozen times before I was able to figure out what was happening, so I wanted to spare you the pain I’ve already endured.

 

The Best Of Both Worlds

Quite possibly my favorite feature of JavaScript is its asynchronous nature. It makes the language so much fun to work with because you can execute things without blocking the user’s interaction. It’s also one of my least favorite things because when you don’t need it, async issues can turn your code into a jacked up Rubix Cube. 


he Best Of Both Worlds

                                                      “Oh When” by Peanut Dela Cruz 

It’s fine if you know how to solve a Rubix Cube, but if you don’t, may God have mercy on your soul.

Fortunately, async/await and promises are here to absolve you of your sins so you don’t ever have to visit callback hell. Bless you, my child. To understand  how to handle JavaScripts with async/await and promises more clearly you can try these examples from Github.

 

Burke

Burke Holland

Blog author

Burke Holland is a front-end developer living in Nashville, TN; the greatest city in the world. He enjoys JavaScript a lot because it's the only way he Node to Express himself. Get it? Never mind. Burke blogs only slightly better than he codes and definitely not as good as he talks about himself in the third person. Burke works on the Azure team at Microsoft on behalf of JavaScript developers everywhere.


Website : https://github.com/burkeholland

Leave a Reply

Your email address will not be published. Required fields are marked *

Top comments

Robert Smith

20 November 2018 at 4:52pm
Thanks for sharing information. Your blog has always been a source of great tips and knowledge

SUBSCRIBE OUR BLOG

Follow Us On

Share on

other Blogs

20% Discount