DEV Community

Ryan
Ryan

Posted on

The Path to Conquering Async JavaScript

Yoooo, I'm glad you're here! We'll be discussing how to conquer asynchronous JavaScript! As I've been trying to teach myself Node.js and build some things, I've found that async calls aren't the most trivial to learn to deal with. Async JavaScript actually takes a good amount of thought to fully comprehend. I hope to pave a path that makes understanding how to deal with async calls faster and easier.

You can find all of the code used in this article on GitHub.


What's the Problem?

JavaScript is a synchronous language, which means it is single-threaded, so it only runs one block of code at a time. A problem occurs when we want to make some sort of async call, which is a multi-threaded. The problem is that when our JavaScript calls an async function - our JavaScript continues to run, although there is a block of code running somewhere else. I run into this problem the most when I am dealing with API requests.

Here's an example -

  1. The JavaScript block starts
  2. An API request is made
  3. JavaScript code continues AND the API request processes
  4. JavaScript uses the request's response before the response is returned

AsyncReq

Notice how there are two number threes? That's the problem. The JavaScript code continues to run as the request is being made. This means it's possible for the JavaScript to attempt to use the request's response value before it's available, and we take the L.


The Goal

The goal is to be able to call async functions in a synchronous fashion - the calls should wait for the one before it to finish before executing:

TheGoal

Which will look something like this when there are multiple async calls:

var a = await asyncToGetA();
var b = await asyncToGetB(a);
alert(b);
Enter fullscreen mode Exit fullscreen mode

Using Callbacks

What's a Callback?

So how do we overcome this issue? Well, let's first take a look at callback functions so that we can get a glance at a potential fix. Callbacks are a way of telling the code to run a function after another function is finished, if your code isn't making too many async calls then this is an ok option to use. This is achieved by passing a function into another function as an argument, and then calling the argument function at the end of the one it's passed to.

Let's say we have a function, runThisFirst(), that we want to run before another function, runThisSecond(). runThisFirst() will simulate an async call with setTimeout() and set x to 5. Once that's finished, runThisSecond() will run. Since we want runThisSecond() to run after runThisFirst(), we are going to pass it as the callback function:

// Define functions
var runThisFirst = function(callback){
    setTimeout(function(){ 
        x = 5;
        callback(); // runThisSecond is called
    }, 3000);
}

var runThisSecond = function(){
    alert(x);
}

// Run functions, pass runThisSecond as the callback argument 
var x;
runThisFirst(runThisSecond);
Enter fullscreen mode Exit fullscreen mode

You can run this code snippet on JSFiddle.

Callback Chaining

If callbacks fix our async issue, then can't we just chain callbacks together? You can, but it gets scary. There's this concept of Callback Hell where the callback JavaScript code turns into a pyramid shape, making it messy and hard to understand.

Here's a minimalist example of what the skeleton of a Callback Hell pyramid looks like:

function one() {
  setTimeout(function() {
    console.log('1. First thing setting up second thing');
    setTimeout(function() {
      console.log('2. Second thing setting up third thing');
      setTimeout(function() {
        console.log('3. Third thing setting up fourth thing');
        setTimeout(function() {
          console.log('4. Fourth thing');
        }, 2000);
      }, 2000);
    }, 2000);
  }, 2000);
};
Enter fullscreen mode Exit fullscreen mode

One of the best programming practices is writing readable code, and callbacks can stray us away from that when chaining too much. To avoid this, we're going to look into Promises and Async/Await.


Promises

A promise function is a function that promises to return a value. This allows you to associate code with async calls, all by having the async calls be apart of the Promise. This is where we can make our API calls. :) Here's how they work:

var somePromise = new Promise((resolve, reject) => {
  var x = 5;
  // Now wait a bit for an "async" call
  setTimeout(function(){
    resolve(x); // Return your promise!
  }, 3000);
});
Enter fullscreen mode Exit fullscreen mode

You can see the Promise constructor has two parameters: resolve, and reject. If everything within the Promise goes according to plan (there are no errors), resolve is called, which returns some value for the Promise. If an error occurs, the Promise should call reject and return the error. For this example, reject is not being called.

Now, let's try to run something that depends on this Promise to see if it waits for the x value to be resolved before executing. We can do this by using the .then function:

var somePromise = new Promise((resolve, reject) => {
  var x = 5;
  // Now wait a bit for an "async" call
  setTimeout(function(){
    resolve(x); // Return your promise!
  }, 3000);
});

somePromise.then((somePromisesReturnValue) => {
  alert("Check it out: " + somePromisesReturnValue);
});
Enter fullscreen mode Exit fullscreen mode

You can run this code snippet on JSFiddle.

Check it out! Things are already looking cleaner and easier to understand. Nice job. :) But now, what if one Promise depends on another Promise? We'll have to chain Promises together.

In order to pass values from one Promise to another, we are going to wrap the Promise within a function like so:

function somePromise() {
  var promise = new Promise((resolve, reject) => {
    var x = 5;
    // Now wait a bit for an "async" call
    setTimeout(function() {
      resolve(x); // Return your promise!
    }, 3000);
  });
  return promise;
}
Enter fullscreen mode Exit fullscreen mode

Promise Chaining

Now we can write another Promise, anotherPromise(), which is going to take the return value of somePromise() and add 1 to it. This function is going to have a shorter setTimeout(), so we can tell that it waits for somePromise() to resolve before running. Notice how we pass somePromisesReturnValue as an argument:

function anotherPromise(somePromisesReturnValue) {
  var promise = new Promise((resolve, reject) => {
    var y = somePromisesReturnValue + 1; // 6
    // Now wait a bit for an "async" call
    setTimeout(function() {
      alert("Resolving: " + y);
      resolve(y); // Return your promise!
    }, 1000);
  });
  return promise;
}
Enter fullscreen mode Exit fullscreen mode

Now, all we have to do is use the .then function in order to call these Promises synchronously:

function somePromise() {
  var promise = new Promise((resolve, reject) => {
    var x = 5;
    // Now wait a bit for an "async" call
    setTimeout(function() {
      resolve(x); // Return your promise!
    }, 3000);
  });
  return promise;
}

function anotherPromise(somePromisesReturnValue) {
  var promise = new Promise((resolve, reject) => {
    var y = somePromisesReturnValue + 1; // 6
    // Now wait a bit for an "async" call
    setTimeout(function() {
      alert("Resolving: " + y);
      resolve(y); // Return your promise!
    }, 1000);
  });
  return promise;
}

somePromise().then(anotherPromise); 
Enter fullscreen mode Exit fullscreen mode

You can run this code snippet on JSFiddle.

Heck yeah! You can see that anotherPromise() waited for somePromise()'s return value, 5, before it executed its code. Things are really looking up. :)


Async/Await

Awesome! So we're done, right? Nope, but we're close! If we take our code from the last section, and try to assign the return value from the Promise chain, we can see that the rest of the code isn't waiting for the entire Promise chain to resolve. "[object Promise]" is alerted first.

function somePromise() {
  var promise = new Promise((resolve, reject) => {
    var x = 5;
    // Now wait a bit for an "async" call
    setTimeout(function() {
      resolve(x); // Return your promise!
    }, 3000);
  });
  return promise;
}

function anotherPromise(somePromisesReturnValue) {
  var promise = new Promise((resolve, reject) => {
    var y = somePromisesReturnValue + 1; // 6
    // Now wait a bit for an "async" call
    setTimeout(function() {
      alert("Resolving: " + y);
      resolve(y); // Return your promise!
    }, 1000);
  });
  return promise;
}

var chainValue = somePromise().then(anotherPromise);
alert(chainValue); // This is executing before chainValue is resolved
Enter fullscreen mode Exit fullscreen mode

You can run this code snippet on JSFiddle.

How do we make the rest of the code wait?! That's where async and await come in. The async function declaration defines an async function, a function that can make async calls. The await operator is used to wait for a Promise to resolve, it can only be used inside an async function.

Mission Accomplished

Instead of using .then, let's create a main() function so that we can make calls like the goal we had at the beginning of the article:

function somePromise() {
  var promise = new Promise((resolve, reject) => {
    var x = 5;
    // Now wait a bit for an "async" call
    setTimeout(function() {
      resolve(x); // Return your promise!
    }, 3000);
  });
  return promise;
}

function anotherPromise(somePromisesReturnValue) {
  var promise = new Promise((resolve, reject) => {
    var y = somePromisesReturnValue + 1; // 6
    // Now wait a bit for an "async" call
    setTimeout(function() {
      resolve(y); // Return your promise!
    }, 1000);
  });
  return promise;
}

const main = async () => {
  var a = await somePromise();
  var b = await anotherPromise(a);
  alert(b);
}
main();
Enter fullscreen mode Exit fullscreen mode

You can run this code snippet on JSFiddle.

Look how pretty that main function is :') beautiful. And there you have it, a nice looking main function that isn't a pyramid. Congratulations!


Adding Broad Error Handling

You may want to add some error handling within your Promises themselves while using the reject callback, but you can also add overall error handling with a try/catch inside of the main() function that will catch any errors thrown throughout all of the code used within the main() function:

const main = async () => {
  try{
    var a = await somePromise();
    var b = await anotherPromise(a);
    alert(b);
  }
  catch(err){
    alert('Oh no! Something went wrong! ERROR: ' + err);
  }
}
Enter fullscreen mode Exit fullscreen mode

We can check this by throwing an error within our anotherPromise():

function somePromise() {
  var promise = new Promise((resolve, reject) => {
    var x = 5;
    // Now wait a bit for an "async" call
    setTimeout(function() {
      resolve(x); // Return your promise!
    }, 3000);
  });
  return promise;
}

function anotherPromise(somePromisesReturnValue) {
  var promise = new Promise((resolve, reject) => {
    var y = somePromisesReturnValue + 1; // 6
    throw 3292; // ERROR CODE BEING THROWN HERE
    setTimeout(function() {
      resolve(y);
    }, 1000);
  });
  return promise;
}

const main = async () => {
  try{
    var a = await somePromise();
    var b = await anotherPromise(a);
    alert(b);
  }
  catch(err){
    alert('Oh no! Something went wrong! ERROR: ' + err);
  }
}
main();
Enter fullscreen mode Exit fullscreen mode

You can run this code snippet on JSFiddle.


Review

I'm glad that we were able to make it this far and come up with a pretty basic path for overcoming JavaScript async problems! We took a look at fixing async issues with callbacks, which can work if there isn't too much complexity. Then we dove into solving the problem with combining Promises and Async/Await! Finally, we talked about how to broadly handle errors. If you would like to learn more about error handling with Promises and Async/Await, I suggest you check out some documentation: Promise.prototype.catch() and await.
If you'd like to work on something where this async functionality could be useful, think about checking out my article on how to make a Twitter bot with Node.js. :)

Top comments (1)

Collapse
 
lepinekong profile image
lepinekong

Good tut but the software industry is walking on his head : look like programming languages are designed by random coïncidence because the intent was so simple : just chaining functions one after another and the implementation absolutly sucks so that a multiple pages tutorial is needed :)