DEV Community

Cover image for Introduction to Asynchronous JavaScript
Christopher Glikpo
Christopher Glikpo

Posted on

Introduction to Asynchronous JavaScript

Introduction

Asynchronous behavior is common in JavaScript, which might be perplexing for programmers who have only worked with synchronous code. This article will explain what asynchronous code is, as well as some of the challenges that come with utilizing it and how to overcome them.

Synchronous Code

If you have two lines of code (a followed by b) in a synchronous program, b cannot start running until a has ended.

Imagine yourself in a long queue of people waiting to purchase bus tickets. You can't start purchasing a bus ticket until everyone in front of you have finished buying theirs.Similarly, the individuals in front of you cannot begin purchasing tickets until you have completed yours.

Asynchronous Code

You can have two lines of code in an asynchronous program (a followed by b), where a schedules a job to run in the future but b executes before that task completes.

Consider yourself as if you were dining at a sit-down restaurant. Other individuals place their meal orders. You may also place a meal order.You don't have to wait for them to complete eating and receive their meal before placing your order. Other individuals don't have to wait for you to finish eating and obtain your food before they can order. As soon as the food is ready, everyone will receive it.

The order in which customers receive their meals is frequently associated with the order in which they placed their orders, although these sequences are not always the same.For example, if you order a steak and I order a glass of water, I'll probably get my order first because serving a drink of water doesn't take nearly as long as preparing and serving a steak.

It's important to note that asynchronous does not imply concurrent or multi-threaded. Asynchronous programming is possible with JavaScript, however it is usually single-threaded.This is similar to a restaurant where a single employee is responsible for all of the waiting and cooking. However, if single employee works swiftly and efficiently switches between jobs, the restaurant seems to have several employees.

Examples

The setTimeout function is probably the simplest way to asynchronously schedule code to run in the future:

// Say "Hi."
console.log("Hi.");
// Say "Good afternoon" 5 seconds from now.
setTimeout(function() {
  console.log("Good afternoon");
}, 5000);
// Say "Hi again!"
console.log("Hi again!");
Enter fullscreen mode Exit fullscreen mode

If you are only familiar with synchronous code, you might expect the code above to behave in the following way:

  • Say "Hi".
  • Do nothing for five seconds.
  • Say "Good afternoon"
  • Say "Hi again!"

But setTimeout does not pause the execution of the code. It only schedules something to happen in the future, and then immediately continues to the next line.

  • Say "Hi."
  • Say "Hi again!"
  • Do nothing for five seconds.
  • Say "Good afternoon"

Getting Data from AJAX Requests

Confusion between the behavior of synchronous code and asynchronous code is a common problem for beginners dealing with AJAX requests in JavaScript. Often, they will write jQuery code that looks something like this:

function getData() {
  var result;
  $.get("process.php", function(response) {
    result = response;
  });
  return result;
}

var result = getData();
console.log("The result is: " + result);
Enter fullscreen mode Exit fullscreen mode

This does not behave as you would expect from a synchronous point-of-view. Similar to setTimeout in the example above, $.get does not pause the execution of the code, it just schedules some code to run once the server responds. That means the return result; line will run before result = response, so the code above will always print "The data is: undefined".

Asynchronous code needs to be structured in a different way than synchronous code, and the most basic way to do that is with callback function

Callback Functions

Let's say you call your friend and ask him for some information, say, a mutual friend's mailing address that you have lost. Your friend doesn't have this information memorized, so he has to find his address book and look up the address. This might take him a few minutes. There are different strategies for how you can proceed:

  • (Synchronous) You stay on the phone with him and wait while he is looking.
  • (Asynchronous) You tell your friend to call you back once he has the information. Meanwhile, you can focus all of your attention on the other tasks you need to get done, like folding laundry and washing the dishes. In JavaScript, we can create a callback function that we pass into an asynchronous function, which will be called once the task is completed.

That is, instead of

var result = getData();
console.log("The result is: " + result);
Enter fullscreen mode Exit fullscreen mode

we will pass in a callback function to getData:

getData(function(result) {
  console.log("The result is: " + result);
});
Enter fullscreen mode Exit fullscreen mode

Of course, how does getData know that we're passing in a function? How does it get called, and how is the result parameter populated? Right now, none of this is happening; we need to change the getData function as well, so it will know that a callback function is its parameter.

function getData(callback) {
  $.get("process.php", function(response) {
    callback(response);
  });
}
Enter fullscreen mode Exit fullscreen mode

You'll notice that we were already passing in a callback function to $.get before, perhaps without realizing what it was. We also passed in a callback to the setTimeout(callback, delay) function in the first example.

Since $.get already accepts a callback, we don't need to manually create another one in getData, we can just directly pass in the callback that we were given:

function getData(callback) {
  $.get("process.php", callback);
}
Enter fullscreen mode Exit fullscreen mode

Callback functions are used very frequently in JavaScript and, if you've spent any amount of time writing code in JavaScript, it's highly likely that you have used them (perhaps inadvertently). Almost all web applications will make use of callbacks, either through events (e.g. window.onclick), setTimeout, and setInterval, or AJAX requests.

Common Problem: Trying to Avoid Asynchronous Code Altogether

Some people decide that dealing with asynchronous code is too complicated to work with, so they try to make everything synchronous. For example, instead of using setTimeout, you could create a synchronous function to do nothing for a certain amount of time:

function pause(duration) {
  let start = new Date().getTime();
  while (new Date().getTime() - start < duration);
}
Enter fullscreen mode Exit fullscreen mode

Similarly, when doing an AJAX call, it is possible to set an option to make the call synchronous rather than asynchronous (although this option is slowly losing browser support). There are also synchronous alternatives to many asynchronous functions in Node.js.

Trying to avoid asynchronous code and replacing it with synchronous code is almost always a bad idea in JavaScript

because JavaScript only has a single thread (except when using Web Workers). That means the webpage will be unresponsive while the script is running. If you use the synchronous pause function above or a synchronous AJAX call, then the user will not be able to do anything while they are running.

The issue is even worse when using server-side JavaScript: the server will not be able to respond to any requests while waiting for synchronous functions to complete, which means that every user making a request to the server will have to wait to get a response.

Common Problem: Scope Issues with Callbacks Inside Loops

When you create a callback inside of a for-loop, you might encounter some unexpected behavior. Think about what you would expect the code below to do, and then try running it in your browser's JavaScript console.

for (var i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i + " second(s) elapsed");
  }, i * 1000);
}
Enter fullscreen mode Exit fullscreen mode

The code above is likely intended to output the following messages, with a second of delay between each message:

1 second(s) elapsed.
2 second(s) elapsed.
3 second(s) elapsed.

Enter fullscreen mode Exit fullscreen mode

But the code actually outputs the following:

4 second(s) elapsed.
4 second(s) elapsed.
4 second(s) elapsed.
Enter fullscreen mode Exit fullscreen mode

The problem is that console.log(i + " second(s) elapsed"); is in the callback of an asynchronous function. By the time it runs, the for-loop will have already terminated and the variable i will be equal to 4.

There are various workarounds to this problem, but the most common one is to wrap the call to setTimeout in a closure, which will create a new scope with a different i in each iteration:

for (var i = 1; i <= 3; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i + " second(s) elapsed");
    }, i * 1000);
  })(i);
}
Enter fullscreen mode Exit fullscreen mode

If you are using ECMAScript6 or later, then a more elegant solution is to use let instead of var, since let creates a new scope for i in each iteration:

for (let i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i + " second(s) elapsed");
  }, i * 1000);
}
Enter fullscreen mode Exit fullscreen mode

Common Problem: Callback Hell

Sometimes you have a series of tasks where each step depends on the results of the previous step. This is a very straightforward thing to deal with in synchronous code:

var text = readFile(fileName),
  tokens = tokenize(text),
  parseTree = parse(tokens),
  optimizedTree = optimize(parseTree),
  output = evaluate(optimizedTree);
console.log(output);
Enter fullscreen mode Exit fullscreen mode

When you try to do this in asynchronous code, it's easy to run into callback hell, a common problem where you have callback functions deeply nested inside of each other. Node.js code and front-end applications with lots of AJAX calls are particularly susceptible to end-up looking something like this:

readFile(fileName, function(text) {
  tokenize(text, function(tokens) {
    parse(tokens, function(parseTree) {
      optimize(parseTree, function(optimizedTree) {
        evaluate(optimizedTree, function(output) {
          console.log(output);
        });
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

This kind of code is difficult to read and can be a real pain to try to reorganize whenever you need to make changes to it. If you have deeply nested callbacks like this, it is usually a good idea to arrange the code differently. There are several different strategies for refactoring deeply nested callbacks.

Split the Code into Different Functions with Appropriate Names

You can give names to the callback functions so that you can reference them by their names. This helps to make the code more shallow, and it also naturally divides the code into small logical sections.

function readFinish(text) {
  tokenize(text, tokenizeFinish);
}
function tokenizeFinish(tokens) {
  parse(tokens, parseFinish);
}
function parseFinish(parseTree) {
  optimize(parseTree, optimizeFinish);
}
function optimizeFinish(optimizedTree) {
  evalutate(optimizedTree, evaluateFinish);
}
function evaluateFinish(output) {
  console.log(output);
}
readFile(fileName, readFinish);

Enter fullscreen mode Exit fullscreen mode

Create a Function to Run a Pipeline of Tasks.

This solution is not as flexible as the one above, but if you have a simple pipeline of asynchronous functions you can create a utility function that takes an array of tasks and executes them one after another.

function performTasks(input, tasks) {
  if (tasks.length === 1) return tasks[0](input);
  tasks[0](input, function(output) {
    performTasks(output, tasks.slice(1)); //Performs the tasks in the 'tasks[]' array
  });
}
performTasks(fileName, [
  readFile,
  token,
  parse,
  optimize,
  evaluate,
  function(output) {
    console.log(output);
  }
]);
Enter fullscreen mode Exit fullscreen mode

Tools for Dealing with Asynchronous Code

Async Libraries

If you are using lots of asynchronous functions, it can be worthwhile to use an asynchronous function library, instead of having to create your own utility functions. Async.js is a popular library that has many useful tools for dealing with asynchronous code.

Promises

Promises are a popular way of getting rid of callback hell. Originally it was a type of construct introduced by JavaScript libraries like Q and when.js, but these types of libraries became popular enough that promises are now provided natively in ECMAScript 6.

The idea is that instead of using functions that accept input and a callback, we make a function that returns a promise object, that is, an object representing a value that is intended to exist in the future.

For example, suppose we start with a getData function that makes an AJAX request and uses a callback in the usual way:

function getData(options, callback) {
  $.get("process.php", options, function(response) {
    callback(null, JSON.parse(response));
  }, function() {
    callback(new Error("AJAX request failed!"));
  });
}

// usage
getData({name: "John"}, function(err, data) {
  if(err) {
    console.log("Error! " + err.toString())
  } else {
    console.log(data);
  }
})
Enter fullscreen mode Exit fullscreen mode

We can change the getData function so that it returns a promise. We can create a promise with new Promise(callback), where callback is a function with two arguments: resolve and reject. We will call resolve if we successfully obtain the data. If something goes wrong, we will call reject.

Once we have a function that returns a promise, we can use the .then method on it to specify what should happen once resolve or reject is called.

function getData(options) {
  return new Promise(function(resolve, reject) {                    //create a new promise
    $.get("process.php", options, function(response) {
      resolve(JSON.parse(response));                                //in case everything goes as planned
    }, function() {
      reject(new Error("AJAX request failed!"));                    //in case something goes wrong
    });
  });
}

// usage
getData({name: "John"}).then(function(data) {
  console.log(data)
}, function(err) {
  console.log("Error! " + err);
});
Enter fullscreen mode Exit fullscreen mode

The error handling feels a bit nicer, but it's difficult to see how we are making things any better given the size of the function. The advantage is clearer when we rewrite our callback hell example using promises:

readFile("fileName")
  .then(function(text) {
    return tokenize(text);
  })
  .then(function(tokens) {
    return parse(tokens);
  })
  .then(function(parseTree) {
    return optimize(parseTree);
  })
  .then(function(optimizedTree) {
    return evaluate(optimizedTree);
  })
  .then(function(output) {
    console.log(output);
  });

Enter fullscreen mode Exit fullscreen mode

Conclusion

At this point, you should be familiar with strategies for confronting some of the difficulties that arise when using asynchronous code.

If you've reached this point, thank you very much. I hope that this tutorial has been helpful for you and I'll see you all in the next.

If you like my work, please consider
Buy me a coffee
so that I can bring more projects, more articles for you

If you want to learn more about Web Development, feel free to follow me on Youtube!

Top comments (0)