Skip to main content

Command Palette

Search for a command to run...

Synchronous vs Asynchronous JavaScript

Updated
10 min read
Synchronous vs Asynchronous JavaScript

You know that one person at the office who cannot move on to the next task until the current one is 100% done? Printer jammed? They stand there. Waiting. Staring. The whole queue behind them has frozen.

That is synchronous JavaScript.

And then there is that other person who drops a print job, goes back to their desk, makes chai, replies to two emails, and only walks back to the printer when it beeps. Still gets everything done. Nothing was blocked.

That is asynchronous JavaScript.

In this post, we are going to understand both of these approaches properly. Not just what they mean, but why JavaScript even needs both, what goes wrong when you pick the wrong one, and how to think about them without tying your brain in a knot.

Let's get into it.

What Is Synchronous Code?

Synchronous code runs line by line, in order. One line finishes completely before the next line starts. No skipping ahead, no doing two things at once.

console.log("Step 1: Boiling water");
console.log("Step 2: Adding tea leaves");
console.log("Step 3: Pouring in cup");

Output:

Step 1: Boiling water
Step 2: Adding tea leaves
Step 3: Pouring in cup

Simple. Predictable. Exactly what you expect.

This is how most beginner code works, and that is totally fine. For simple tasks, synchronous code is readable and easy to follow.

Here is another example:

function addTax(price) {
  return price * 1.18; // 18% GST, very relatable
}

let originalPrice = 500;
let finalPrice = addTax(originalPrice);
console.log("You owe: ₹" + finalPrice);

Each line waits for the previous one. addTax is called, it runs completely, returns the value, and then the next line uses it. That is synchronous execution.

What Is the Call Stack?

When JavaScript runs your code, it uses something called the call stack to keep track of what is currently executing. Think of it like a stack of plates. You add a plate (function call) on top, it runs, and when it is done, you take it off the stack. The plate below it resumes.

[ addTax ]       <-- currently running
[ main script ]  <-- waiting

Once addTax finishes, it gets removed. The main script continues. One thing at a time. That is the fundamental nature of JavaScript: it is single-threaded.

And this is exactly why asynchronous behavior becomes necessary. But more on that in a second.

What Is Asynchronous Code?

Asynchronous code does not wait. It says "start this task, and when it is done, come back and handle the result. Meanwhile, I will keep moving."

The classic example is setTimeout:

console.log("Order placed");

setTimeout(function() {
  console.log("Your Zomato order has arrived!");
}, 3000);

console.log("Watching reels while waiting...");

Output:

Order placed
Watching reels while waiting...
Your Zomato order has arrived!  (after 3 seconds)

Wait, what? The third console.log ran before the one inside setTimeout? Even though setTimeout was written second?

Yes. Because setTimeout is asynchronous. JavaScript did not stop and stare at the timer for 3 seconds. It registered the timer, moved on, and came back to run the callback once the time was up.

This is asynchronous behavior. Non-blocking. Keep moving, handle results when they are ready.

Why Does JavaScript Need Asynchronous Behavior?

Here is the thing. JavaScript runs in a single thread. There is no "background thread" doing work while the main thread does other stuff. It is one lane of traffic.

So imagine you make a request to fetch some data from a server. That request might take 2 seconds. If JavaScript just sat there, frozen, waiting for those 2 seconds to pass, your entire web page would be unresponsive. No scrolling. No clicking. No nothing. Just a blank, frozen screen.

That would be a terrible user experience.

Asynchronous behavior solves this. Instead of blocking the thread, JavaScript says: "I have sent the request. I will continue doing other things. When the server responds, I will handle it."

This is made possible by a combination of:

  • Web APIs (built into the browser, not part of JavaScript itself) that handle things like timers, HTTP requests, and DOM events

  • The Callback Queue (also called the task queue or message queue) where completed async tasks wait

  • The Event Loop, which constantly checks: "Is the call stack empty? Is there anything waiting in the queue? If yes, push it to the stack."

A Quick Visual

JavaScript Engine
|
+-- Call Stack         <-- where your code runs
|
+-- Web APIs           <-- where async tasks are handled (browser's job)
    |
    +-- Callback Queue <-- where results wait once async tasks finish
         |
         +-- Event Loop (the bouncer) <-- moves things from queue to stack

This is why JavaScript can handle network requests, file reads, timers, and user events without freezing your entire page.

Blocking vs Non-Blocking Code

Let's really understand what "blocking" means, because this is the heart of the whole concept.

Blocking Code

function slowTask() {
  let start = Date.now();
  while (Date.now() - start < 3000) {
    // doing absolutely nothing for 3 seconds
    // burning CPU like a laptop on a blanket
  }
  console.log("Finally done!");
}

console.log("Before slow task");

slowTask();

console.log("After slow task");

Output:

Before slow task
(3 seconds of silence and regret)
Finally done!
After slow task

During those 3 seconds, your entire page is frozen. Nobody can click anything. The browser might even show "Page Unresponsive." You have blocked the single thread.

This is a while loop that just spins doing nothing useful. It is like standing in a billing counter queue and when you reach the front, you decide to count all your coins one by one, out loud, while everyone behind you stares at the back of your head.

Non-Blocking Code

console.log("Before async task");

setTimeout(function() {
  console.log("Async task done!");
}, 3000);

console.log("After async task");

Output:

Before async task
After async task
Async task done!  (3 seconds later)

Non-blocking. The setTimeout was handed off to the Web API. JavaScript moved on. When the timer finished, the callback got queued and then executed. Nobody was frozen.

Real World Examples of Async Code

1. Fetching Data from an API

This is the most common real-world async use case you will encounter.

console.log("Starting fetch...");

fetch("https://jsonplaceholder.typicode.com/todos/1")
  .then(function(response) {
    return response.json();
  })
  .then(function(data) {
    console.log("Got data:", data.title);
  });

console.log("Fetch was called, moving on...");

Output:

Starting fetch...
Fetch was called, moving on...
Got data: delectus aut autem  (whenever the server responds)

The fetch call goes out to the network. JavaScript does not sit there waiting for the server. It continues to the next line. When the response comes back, the .then callbacks are executed.

This is why .then() exists on Promises. It says: "When this is done, then do this."

2. Reading a File (Node.js)

const fs = require("fs");

console.log("Starting to read file...");

fs.readFile("menu.txt", "utf8", function(err, data) {
  if (err) {
    console.log("Error reading file");
    return;
  }
  console.log("File contents:", data);
});

console.log("readFile was called, not waiting...");

Output:

Starting to read file...
readFile was called, not waiting...
File contents: ...  (once the OS reads the file)

Same idea. The file read is handed off. JavaScript moves on. Callback fires when ready.

3. User Events

document.getElementById("submitBtn").addEventListener("click", function() {
  console.log("Button was clicked!");
});

console.log("Event listener registered, not blocking anything.");

The event listener does not block anything. It just registers: "When this button is clicked, run this function." Meanwhile, the rest of your code runs normally. When the user clicks, the callback fires.

Problems That Occur With Blocking Code

Let us look at some specific problems blocking code causes.

Problem 1: Frozen UI

If you run a heavy synchronous operation on the main thread in a browser, the UI completely freezes. This is one of the most common performance bugs in frontend code.

// DO NOT do this for heavy tasks
function processMillionRecords(records) {
  let results = [];
  for (let i = 0; i < records.length; i++) {
    results.push(records[i] * 2); // imagine this is a heavy calculation
  }
  return results;
}

If records has a million items, this loop will run synchronously, block the thread, and your user is stuck staring at a frozen page. No scroll. No click. Just pain.

Problem 2: Callback Hell (When Async Goes Wrong Too)

Okay, so async is good. But when people started writing async code using nested callbacks, things got ugly fast. This is called "callback hell" or the "pyramid of doom."

getUser(userId, function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetails(orders[0].id, function(details) {
      getProductInfo(details.productId, function(product) {
        console.log("Finally got the product:", product.name);
        // and we are now 4 levels deep
        // good luck debugging this
      });
    });
  });
});

Each nested callback depends on the previous one finishing. The code keeps shifting to the right. It becomes almost impossible to read, maintain, or debug.

This is exactly why Promises were introduced in ES2015, and then async/await in ES2017. They were designed to make async code readable and linear, without the pyramid.

Problem 3: Race Conditions

When you have multiple async operations and you assume they will finish in a certain order, but they do not, you get race conditions.

let userName = "";

setTimeout(function() {
  userName = "Ravi";
}, 1000);

console.log("Hello, " + userName); // prints "Hello, " -- userName is still empty!

You started the timer, but then immediately tried to use userName before it was set. The async operation had not finished yet. This is a classic beginner mistake.

The fix is to use the value inside the callback (or a Promise/async-await pattern), not outside it.

setTimeout(function() {
  let userName = "Ravi";
  console.log("Hello, " + userName); // prints correctly
}, 1000);

Sync vs Async: When to Use What

Situation Use
Simple calculations, string manipulation, logic Synchronous
Fetching data from an API Asynchronous
Reading/writing files Asynchronous
Timers and delays Asynchronous
DOM manipulation after data loads Asynchronous
Short utility functions Synchronous

As a general rule: if the task involves waiting (network, file system, timers), go async. If it is pure computation that finishes instantly, sync is fine.

Final Example

Here is a simple async example using async/await, which is the modern clean way to write async code:

async function getUserData() {
  console.log("Fetching user info...");

  try {
    let response = await fetch("https://jsonplaceholder.typicode.com/users/1");
    let user = await response.json();
    console.log("User name:", user.name);
  } catch (error) {
    console.log("Something went wrong:", error.message);
  }
}

getUserData();
console.log("This line runs while the fetch is happening...");

Output:

Fetching user info...
This line runs while the fetch is happening...
User name: Leanne Graham  (once the response arrives)

async/await looks synchronous. It reads top to bottom. But under the hood, it is still asynchronous. await pauses only the async function itself, not the entire thread. The rest of your code keeps running.

This is the sweet spot: async behavior with clean, readable code.

See you in the next one.