Skip to content

Asynchronous JavaScript

Jacob Mai Peng edited this page Jun 27, 2021 · 1 revision

Asynchronous programming can be tricky to wrap your head around for beginners. In web programming, I find it easiest to think of it as a way to handle operations that take a long time.

Slow operations

Here are some real-life examples of operations that can take very long to execute:

  • Communicating over a network. You have to do this whenever you communicate with a database or server, and in general network communication is very slow. Even the fastest internet speeds are orders of magnitude slower than operations like calling a function or assigning to a variable.
  • Reading from or writing to a file. This doesn't always feel like a slow operation, but accessing files on disk will also generally be orders of magnitude slower than accessing memory due to the nature of hardware.

By default, JavaScript executes code synchronously. Lines execute one at a time, with functions producing return values before continuing. Like so:

const myFunction = () => 42;

const myValue = myFunction();
// We *know* that myFunction will have finished executing by the time we get here, so myValue will be defined.
console.log(myValue); // 42

This is all well and good when our programs run very quickly, but if our operations take longer, executing them synchronously will require waiting until they're done before we can move on.

  • In the browser, this results in the whole browser becoming unresponsive. The user will perceive that their browser has frozen, which is seldom an acceptable user experience.
  • On the server, this results in the server getting hung up and being unable to process requests. Servers are often responsible for talking to thousands of clients at once, so an unresponsive server can create big issues for all of your users.

Recall that setTimeout() allows us to schedule events to execute in the future. Here's a simple implementation that executes synchronously:

const mySetTimeout = (fn, ms) => {
  let started_at = Date.now(); // This returns the current time in milliseconds
  while (Date.now() - started_at < ms) {
    // Do nothing as we wait for the timer to elapse.
  }

  fn(); // We're done waiting! We can now call the function.
};

// Careful, this might momentarily freeze your browser.
mySetTimeout(() => console.log('Finally!'), 5000);

To avoid these problems, JavaScript provides ways to write code asynchronously, or async for short. The real setTimeout is a great example of this:

// This won't freeze your browser.
setTimeout(() => console.log('Finally!'), 5000);

console.log("I'm underneath the setTimeout, but I'll get printed first!");

The key here is that rather than manually waiting for the timer to elapse before moving on, setTimeout instructs the internal JS runtime to queue the timer in the background. Since it's in the background, your code will happily continue to execute without worrying about the timer you've queued. Then, once the timer has elapsed, the JS runtime will call your callback for you.

For more information on the "internal JS runtime", this is a fantastic explanation. Highly recommended, but not required.

Aside: other languages might tackle the problem of juggling long-running tasks by using multiple threads to execute multiple lines of code at once. The use of async code provides an alternative to threads that some people find easier to reason about.

Returning values from slow operations

Using setTimeout is a great way of illustrating slow operations, but in the real world, we're not just waiting for timers to elapse for no reason. Instead, we need our asynchronous code to extract values. If we're fetching some data from a server, we'd like to use the data once we've fetched it. To do this, we'll look at XMLHttpRequest, a way to retrieve data from servers.

const xhr = new XMLHttpRequest();
// This is a simple API that returns random Game of Thrones quotes.
// Contrary to the name, "open" just configures the request. It doesn't send it.
xhr.open("GET", "https://game-of-thrones-quotes.herokuapp.com/v1/random");

xhr.onload = () => {
  console.log(`Request completed with status ${xhr.status}: ${xhr.response}`);
  /*
  `xhr.response` should be something like this:
  {
    "sentence": "Fighting bravely for a losing cause is admirable. Fighting for a winning cause is far more rewarding.",
    "character": {
      "name": "Jaime Lannister",
      "slug": "jaime",
      "house": {
        "name": "House Lannister of Casterly Rock",
        "slug": "lannister"
      }
    }
  }
  */
};

xhr.onerror = () => {
  // This triggers if the request couldn't be made at all.
  console.error('There was a network error :(');
};

// The configuration is done, so we can fire off the request.
xhr.send();

The key thing to note here is the xhr.onload property: this is set to a callback that is executed once the network request has completed. If you try to access xhr.response before the request is complete, you won't get any data. The best way to guarantee that your network request has completed is by using the onload callback to read your data.

Promises

So far, we've seen asynchronous code handled by using callbacks. Some background process is started, then when the task is done, we use the results inside of a callback. Callbacks can have certain disadvantages, especially when your program requires performing multiple asynchronous tasks sequentially, such that each task needs to wait for all previous tasks to finish. This is commonly known as "callback hell".

  • This is optional reading. The point here is contained in the first paragraph or so.

An alternative to this is to use Promises. Promises are another way to write asynchronous code in JS that often results in cleaner code than callbacks, especially when you need to chain together multiple async calls.

You can read about Promises and their syntax here.

  • This article is the main reason I introduced XMLHttpRequest above, since it converts the callback-style code to code that uses Promises. As an exercise, see if you can use the new Promise constructor to write a version of setTimeout that returns a promise, like so:
setTimeoutPromise(3000).then(() => console.log("Time's up!"));

Async/Await

The final thing to know about asychronous JS is the async/await syntax, which is a syntactic sugar on top of Promises that make async code even easier to read and write.

You can read about async and await here.

  • Read up to, but not including "Handling async/await slowdown". The rest of the article is optional.

Clone this wiki locally