Mega Guide: Asynchronous vs Synchronous Javascript

This article aims to be the most comprehensive guide to asynchronous vs synchronous in Javascript. This is a very easy topic to understand once you actually know it. And there’s the problem, every single programmer sucks when trying to explain it!

I will initially explain these concepts using the analogy of a shopping trip, which consists of one or more people and a long shopping list.

As I proceed from synchronous to asynchronous concepts, I will weave in the history of processor development. I feel this is important for beginners to know as it gives you the right context around why we try to async almost everything these days.

Finally, we will run through the history of asynchronous code syntax, using mostly Javascript, but also some pseudo code to make things clearer.

Note: There are many nuances on this subject, not least of which is that of CPU architecture. I won’t be touching on the latter, instead simplifying it so you can really understand sync vs async. In fact even simple processors can do clever stuff with a single core but in this article I will treat them as dumb “single task” machines.

Synchronous Programming

What is Synchronous Programming?

Synchronous programming is simple to understand as it works in a linear fashion, the same way our brains do:

Synchronous programming means that a list of computation tasks happen in the order they are defined.

If you have a list of tasks then each one gets fully completed before the next one is even started. Your program will proceed down the list of tasks in this manner, never executing anything out of order.

Synchronous Programming Explained

Let’s start this section with a simple shopping analogy whereby I have a shopping list with 2 items on it, milk and bread.

At the store I cannot get milk and bread at the same time, therefore I must fetch each item sequentially. If my shopping list were to expand to 100 items then the shopping trip would take 50 times as long (ignoring walking and paying time etc).

The basic rule is thus: As long as I’m the only person doing the shopping then each doubling of the list doubles the time required, no matter what.

In programming this is the same as having a single processor (CPU) with one core (sometimes referred to as a thread). This means there is only one worker that can execute code and only one task can be running at any given time.

This single worker concept is where the idea of synchronous code comes from. One worker with one list of tasks needs to do them in a defined order.

Example of Synchronous Code

let todayPrice = getTodaysExchangeRate()
let yesterdayPrice = getYesterdaysExchangeRate()
let deltaPrice = today - yesterday

The above code is synchronous and runs strictly in order, each line completing before the next:

  1. Get todays exchange rate from an external service (API), then store it in “todayPrice” variable
  2. Get yesterdays exchange rate from another API, then store it in “yesterdayPrice” variable
  3. Subtract one from the other and assign the answer to the variable called “deltaPrice”

Now this is a very simple piece of code, but there’s an issue you may have spotted. The services fetching prices may rely on another server across the world. A call to that server could take 2 seconds to return a result which means you wait 4 seconds in total for all results.

As the average attention span is 2 seconds, you’ve already lost your user to YouTube before you even start on the second call!

Negative Impact of Synchronous Code

Imagine if you have a financial trading program and are requesting prices multiple times a second. You could lose millions of dollars in a trade because a server somewhere took 100 milliseconds too long to respond!

Or, if creating a game that runs on a single CPU, you need to prioritise either running the game or reading assets and textures from a disk for the upcoming level. The well known solution for this is “level loading”.

Or, if you have an app that continuously fetches data from disk then it would wreck the user experience, causing stuttering and annoying pauses.

So what’s the solution for running multiple tasks at once? Let’s start by looking at the evolution of processors.

Moving on from Synchronous Programming

Decades ago most processors could generally only run one task at a time (not totally true *) so synchronous programming was a natural fit. After all, if your CPU can only run one task then you cannot have anything running in parallel.

In 2002 that changed when Intel introduced hyper-threading. Hyper threading basically allows you to run 2 processes on one CPU core. This invention allowed the average consumer PC to run multiple instructions at the same time.

Shortly after that Intel even started producing multicore processors, dramatically increasing the thread count. Now you could now run 4x, 8x etc the instructions in parallel.

* Aside: Technically, multi-threading has been around since the 1950s with IBM etc but that hardware was very expensive and specialised.

* Aside 2: CPUs can have multiple “circuits” inside of them, each handling something specialised. Video decode and encode is one example. That’s why your laptop can play a lot of YouTube on battery as the specialised circuit uses very little power. However we’re ignoring this for sake of the article.

* Will the asides ever end? : If your CPU is fast enough you may not actually even notice multiple operations on one core (if the CPU has a decent instruction set). Hence the never ending race for faster and faster processing frequencies.

More Processor Cores. More Workers.

Following Intels multicore multithread revolution we now had multiple workers to do our evil code bidding. To understand why this development was utterly amazing, this let’s go back to our shopping analogy.

Shopping Analogy: If I’m one guy in a shop with one trolley and a 100 item list, I need to fetch everything myself. But if I now have 2 people on the shopping trip I can assign one task to each person to run in parallel and, in theory, we can complete the shopping trip in half the time.

Fundamentally, this increase in cores and threads is where the concept of asynchronous programming started to appear in the mass market *. If you have multiple CPU workers dividing up the task list then you can run things a lot more efficiently.

* Async coding has actually been around for a very long time, but the average developer only really got exposed to it in the early 2000s.

Asynchronous Programming Overview

What is Async Programming?

Asynchronous programming means that a list of tasks gets split between multiple workers, each executing its list at the same time as the other workers, without blocking any of the other workers from doing their tasks.

Async Programming Explained

With the advent of multiple cores and threads, software engineers could now sub-divide their code to be split among the workers.

Each worker would take its list of tasks and process it in parallel to the other workers. In the end all the workers would return their results to be used in the next stage of the program.

A simple example may be fetching ticker prices for stocks. Let’s say you have 100 stocks that you’re watching and each price fetch takes 100 milliseconds (which is really fast). Without async programming that would take 10 seconds on a single core CPU. Clearly no good for us advanced traders…

Now, with multi core CPUs, we can assign those tasks over 16 threads, reducing the time to about half a second.

Please note: I am grossly over simplifying this process – a network call doesn’t actually tie up the CPU thread whilst it is running!

Importance of Asynchronous Code – Time Saving

Clearly, running tasks to fetch price in a parallel manner will save us a lot of time! Nowhere is this more important than on a server. If you have a thousand users connecting every second then you want 2 things:

  1. As many CPU threads as possible to answer simultaneous requests immediately
  2. Fast CPU clock speeds to give users a result before they get bored and go to YouTube

This is why you see monster multicore server CPUs selling for tens of thousands of dollars. For example, Intel’s newest architecture will produce 288 core CPUs! This is called “vertical scaling” and allows each server to scale up to fulfil more and more requests.

However, there is a limit to vertical scaling. No matter the CPU you can’t serve millions of requests per second – that’s when you need horizontal scaling. But that’s a different kind of architecture beyond the scope of this article.

Asynchronous Programming – How to Use It

Now that you understand sync vs async you’re in a good position to deep dive into a real understanding of async code.

In this section we will look at real coding techniques using mostly Javascript to show examples. However, the principles I’m about to show you apply to pretty much all modern languages and frameworks with very little variation.

As with the preceding text we’ll move forward in an historical manner, going over async techniques from previous years. These older techniques will provide you the necessary context so you can fully understand modern coding paradigms like async / await.

Async – Which Worker Organises the Workers?

One thing to cover before we go on is who controls the worker threads? This is generally another CPU thread (sometimes called the main thread – or controller / dispatcher). This thread is from where the tasks are sent to other workers.

The main thread as where you manage your program flow but do none of the heavy lifting.

Generally speaking, different threads of work cannot communicate with one another as each is its own universe. So, if something is calculated by one thread then it cannot share that result to another worker thread. Instead it uses something called a callback, which communicates back to the main thread.

Callbacks – Asynchronous Programming

So, we now have multiple workers doing multiple tasks in parallel, orchestrated by a main thread. The question is – how do we know when a worker finishes a task and how do we get hold of the result?

One of the first answers to this problem was the “callback”. Simply put – when the worker thread finishes its task it calls back to the main worker task with the result.

A callback is usually defined by passing in a function to the arguments when you start the long running task. When the main thread receives a callback it executes the function you passed in.

Callbacks – Code Example

Let’s now convert our synchronous code from earlier into callback based code. Read the below code very carefully including the comments (lines marked //) to understand what’s happening.

let todayPrice = null

//the following function executes WITHOUT waiting for a result. The computer simply sets the task going and IMMEDIATELY moves on to the next lines of code. The section between the curly braces {} is a function that's passed in. It's only executed when getTodaysExchangeRate calls back with a price.

//This type of function is known as an "anonymous function". In Javascript it is generally defined in the format:
// (callbackData) => { code to execute }
getTodaysExchangeRate((price) => {
  this.todayPrice = price
})

let yesterdayPrice = null

//the following function also executes without waiting for a result. The computer simply sets the task going and IMMEDIATELY moves on to the next line of code
getYesterdaysExchangeRate((price) => {
    this.yesterdayPrice = price
})

//The following code checks every 50 milliseconds to see 
//  if the results are in yet from the callbacks.
//FYI: "setInterval" allows you to endlessly loop code every x milliseconds

//When the results are in then it prints the delta price to the console
//Note that this is also a callback function!
setInterval((todayPrice, yesterdayPrice) => {
  if (todayPrice && yesterdayPrice) {
    let deltaPrice = today - yesterday
    this.showDelta (deltaPrice)
    endInterval() //stops running this checking function if prices are in
  }
}, 10)

function showDelta(deltaPrice) {
  console.log(deltaPrice) //print price to terminal line
}


The above code is extremely simplified to be readable by the new programmer so isn’t technically how you’d do things. However, it demonstrates the point I’m trying to make.

The asynchronous order of operations is:

  1. a) getTodaysExchangeRate ( ) goes off to fetch the price on the first processor thread. When it gets the result it will assign it into the “todayPrice” variable

    b) The program will immediately go the next line of code without waiting for the result (as it knows that’s coming later)

    c) getYesterdaysExchangeRate ( ) fetches yesterdays price on a second thread. When it gets that result it assigns it into the “yesterdayPrice” variable

    d) Without waiting for any of the above to complete, a function checks for the results every 50 milliseconds (also looping asynchronously with a callback)
  2. If both results are present then we run the deltaPrice calculation and show it in the command line.

Note: There are much better ways to check for results of multiple asynchronous operations, rather than polling them as we’re doing with setInterval. The above code just makes the concepts easy to understand.

Note 2: Yes I’ve seen polling of async results. Yes, I’ve done it myself. We were all beginners once so don’t beat yourself up if you’ve done it!

Note 3: Code will not technically happen on a new thread for each task, as that decision is taken by your operating system (generally). However, we’re treating it as though it would.

Callback Hell (Pyramid of Doom) – Avoid This

Sometimes you need the result from an async call before you can proceed with the next part of your code.

You would come across this, for example, if you want to make sequential server calls after a user has logged in to your app. You’d perform a series of data fetches, insertion of app content etc that may need to happen sequentially, after a user ID has been obtained.

In the below code we’re going to call up 3 async operations that need to be executed in order. You may wonder why we don’t just use synchronous code for this…but that would impact the main thread remember… hence, even sequential long running tasks should still be offloaded to other threads.

In the below example, after each async function runs, we use the callback to trigger the next async function. Therefore we have a stack of 3 operations which results in nested callbacks (aka: pyramid of doom):

Note that in this example we’re just faking the call time with setTimeouts lasting 1 second:

// Example of callback hell

//START - 3 functions that simulate a long running call
function asyncOperation1(callback) {
  setTimeout(function() {
    console.log("Async Operation 1 completed");
    callback(null, "Result of Async Operation 1");
  }, 1000);
}

function asyncOperation2(callback) {
  setTimeout(function() {
    console.log("Async Operation 2 completed");
    callback(null, "Result of Async Operation 2");
  }, 1000);
}

function asyncOperation3(callback) {
  setTimeout(function() {
    console.log("Async Operation 3 completed");
    callback(null, "Result of Async Operation 3");
  }, 1000);
}
//END - 3 functions

// Nested callbacks, each one depending on result of previous.#
asyncOperation1(function(error1, result1) {
  if (error1) {
    console.error("Error in Async Operation 1:", error1);
    return;
  }
  console.log(result1);

  asyncOperation2(function(error2, result2) {
    if (error2) {
      console.error("Error in Async Operation 2:", error2);
      return;
    }
    console.log(result2);

    asyncOperation3(function(error3, result3) {
      if (error3) {
        console.error("Error in Async Operation 3:", error3);
        return;
      }
      console.log(result3);

      // More nested callbacks could be added, leading to further indentation and complexity
    });
  });
});

The above code is horrendously hard to read with just 3 callbacks! Imagine if we had 10 of them! Therefore we need a better way to do this. Enter promises ->>>>

Avoid Callback Hell with Promises

A promise is not much different from a callback (it essentially is one) but they are much more manageable. In the previous callback examples we polled results every 50 milliseconds or had nested callbacks but clearly those are not optimal solutions.

With promises we can leave the polling and notifications to be managed by the promise module itself.

Promise definition: A promise is a function that promises to return something in the future.

Promises are such a good fit with the async paradigm that there are promise type libraries available for most languages. For example, in Javascript we would use the natively included promises module.

Let’s now see how they work with some sample code that does the following:

  • Fakes a long running task
  • Creates 5 of those tasks at the same time
  • Uses Promise.all() to callback when all results have been fetched
  • ONLY prints the results if they are all successful.
// Function representing a long running task that returns a promise
function asyncTask(id) {
  return new Promise((resolve, reject) => {
    // Simulating asynchronous operation
    setTimeout(() => {
      console.log(`Task ${id} completed`);
      resolve(`Result of Task ${id}`);
    }, Math.random() * 2000);
  });
}

// Creating an array of tasks that will run simultaneously
const tasks = [
  asyncTask(1),
  asyncTask(2),
  asyncTask(3),
  asyncTask(4),
  asyncTask(5)
];

// Using Promise.all() to start and execute tasks in parallel 
//. and ONLY call back when they are ALL complete
Promise.all(tasks)
  .then(results => {
    console.log("All tasks completed successfully");
    console.log("Results:", results);
  })
  .catch(error => {
    console.error("One or more tasks failed:", error);
  });

Notice how there are no callback indentations littering our code. The only indentations are to catch errors thrown by a failed promise result (using then, catch). This code is also a great example of ACID principles. Using the promise results, you either pass (if all succeeded) or fail (if even just one failed).

This example would be typical if writing to a database. You would want the 5 writes to all be successful to keep data consistent. If one write should fail then you roll back ALL the writes and retry.

Promise libraries can be architected in all kinds of different ways, for example, if you prefer to inspect or use the result from each promise callback in a chain….

Chaining Promises

To chain a list of promise tasks we use the “then” syntax, passing in a success and failure function to handle each condition. The below code chains 3 long running tasks, allowing you to inspect the result at each stage and decide whether to continue or not.

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("foo");
  }, 300);
});

myPromise
  .then(handleFulfilledA, handleRejectedA) //pass in 2 functions that do something according to success / error
  .then(handleFulfilledB, handleRejectedB)
  .then(handleFulfilledC, handleRejectedC);

There are many, many ways to use promises depending on your needs. If you have a special case then your best option is to see the documentation for your particular promises library.

Finally, promise syntax is a lot cleaner than callbacks but there’s still a little indentation and you may need to cross scope to store results. We have finally reached what you’ve been waiting for, async and await.

Asynchronous Programming with Async & Await

No doubt you’ve heard of async / await and is what you’re here for. This is where we currently are in terms of state of the art with async programming (although it’s actually over 15 years old now!)

First, lets recap a little of what you’ve read so far, to understand where we are:

  1. Async programming grew to take advantage of multiple CPU threads
  2. Multiple tasks could now run in parallel
  3. We can track multiple tasks using promises or similar paradigms but they’re not perfect

The above solutions solved some issues but presented their own problems:

  1. Code is more readable with promises but it’s still prone to nesting and a bit difficult to decipher right away, especially for other people on your team.
  2. Storing results from the callback means you need to cross scope which can create some funky errors.
  3. Responsiveness of your app or server can suffer when you accidentally run async operations on the main thread (rare but can happen).

Theoretical solutions to these would be to:

  1. Have few to no indentations in our code (more readable and fewer scoping issues)
  2. Automatically take care of moving heavy loads off the main thread (without having to think about it).

Note: Async / await didn’t appear due to responsiveness or readability issues. It was invented as part of F# and C# as their solution for callbacks / long running tasks. But I have artistic license because it’s my website 😉

How to Use async / await

Below we will fetch some data from a text file on disk to introduce async / await concepts. Read the comments in the code to see what’s going on.

// Define a function with the "async" keyword, which allows it to be "awaited"
// This function returns a promise that resolves some text after 1 second
async function getPriceHistoryFromTextFile() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve("Price = $4.56"), 1000)
  });
}

//define a second async function that will call the above function and alert the result
async function printPriceHistory() {
  // The line below calls our long running function from above 
  // MAGIC SECTION: The "await" keyword  tells our program to suspend execution of this particular thread of code until a result is received from the callback
  // This leaves the main thread free!
  let result = await getPriceHistoryFromTextFile()

  alert(result) // result fetched from above line, resume execution of code
}

printPriceHistory()

The basic principles of async await code:

  • Any long running function that will complete at some unknown point in the future should be marked with the “async” keyword
  • When you call that function you prefix it with the verb “await”
  • Await tells the program to suspend execution of your code until the async operation returns a result
  • This leaves the main thread free!

As you can see this code produces no indents, no manually managed crossing of scope and is much shorter and readable.

What’s more, if you forget to use the await keyword then you will print out a Promise – which is a very easy bug to see and fix (don’t ask how I know this).

The above reasons are why async / await syntax has become the defacto standard for multi-threaded coding.

Summary

We first covered the history of CPUs all the way from single core, to multithread and multicore monsters. This development created a way to run parallel tasks in an application and gave rise to the asynchronous paradigm. This meant improved responsiveness and processing power in software.

However, the rise of asynchronous coding created several issues. How do you get a result and get notified when it’s ready? Callbacks were the first solution for this and work great, but chaining more than 2 requests creates ugly code and scoping issues.

The solution to callbacks was promises which was a syntax solution to these issues. However it still not adequately solve these issues.

Finally we looked at async await, which is just “syntactic sugar” over callbacks and promises but solve the problems defined above. Async & await prevents indentation issues and generally guides you to the right architecture by forcing you use the await keyword.

Ov real it reliably produces a fluid, main thread free user experience!

Leave a Comment