DEV Community

Cover image for Synchronous vs Asynchronous JavaScript
SATYA SOOTAR
SATYA SOOTAR

Posted on

Synchronous vs Asynchronous JavaScript

Hello readers πŸ‘‹, welcome to the 16th blog in this JavaScript series!

Today we are going to talk about a concept that trips up a lot of beginners but, once understood, makes everything about JavaScript feel so much clearer: the difference between synchronous and asynchronous code.

If you have ever wondered why setTimeout doesn't pause your entire program, or why fetching data from an API requires special handling, this is the blog for you. We will start from scratch, use simple examples, and slowly build a mental model that sticks.

What does synchronous mean?

The word "synchronous" simply means things happening one after the other, in sequence. In JavaScript, synchronous code is executed line by line, from top to bottom. Each line waits for the previous line to finish before it runs.

Take this tiny script:

console.log("First");
console.log("Second");
console.log("Third");
Enter fullscreen mode Exit fullscreen mode

You can predict the output perfectly. It will always be:

First
Second
Third
Enter fullscreen mode Exit fullscreen mode

This is synchronous flow at its most basic. The engine cannot move to the next console.log until the current one is completely done. That predictability is great for understanding our code, but it has a hidden downside.

The problem with blocking code

Imagine you are at a coffee shop with a single barista. A customer orders a complicated drink that takes five minutes to prepare. If the barista stands there doing nothing else until that one drink is ready, the line behind that customer will grow, and everyone else will be stuck waiting. That is exactly what happens with synchronous blocking code.

Here is a synchronous example that simulates a heavy task:

console.log("Start");

// Simulate a long-running task
const start = Date.now();
while (Date.now() - start < 3000) {
  // do nothing, just block for 3 seconds
}

console.log("End");
Enter fullscreen mode Exit fullscreen mode

If you run this in a browser, the whole page freezes for three seconds. Buttons won't click, scrolling will lag, and the browser might even show a "page is unresponsive" warning. That is because the single JavaScript thread is stuck inside the while loop and cannot do anything else, not even repaint the screen.

In a real application, this would be disastrous. Imagine fetching data from a server that takes two seconds to respond. If JavaScript blocked the entire page while waiting, the user would see a frozen screen and probably leave. That is precisely why asynchronous behavior exists.

What does asynchronous mean?

Asynchronous code, on the other hand, allows the program to start a task and then move on to the next line without waiting for that task to finish. When the task completes, a callback, a promise, or an event handler takes care of the result. The main thread stays free to keep responding to user interactions, rendering updates, or handling other code.

Let’s look at the most classic asynchronous example: setTimeout.

console.log("Start");

setTimeout(function () {
  console.log("Inside timeout");
}, 2000);

console.log("End");
Enter fullscreen mode Exit fullscreen mode

The output here surprises a lot of newcomers:

Start
End
Inside timeout
Enter fullscreen mode Exit fullscreen mode

Even though setTimeout appears before the last console.log, its callback runs last. The engine does not wait for two seconds before printing "End". It registers the timer, continues executing, and eventually, after two seconds have passed, the callback gets executed. This is non-blocking behavior in action.

Why does JavaScript need async behavior?

JavaScript was born to run inside the browser, and its main job was to react to user interactions: clicks, keypresses, mouse movements. If it blocked the main thread for long periods, the entire UI would become unresponsive. You can't have a button that takes three seconds to react because some data is loading.

Even outside the browser, with Node.js, the same principle applies. A server handling thousands of requests cannot afford to block on a single database query or file read. Asynchronous I/O allows it to start an operation and immediately move on to handle the next request, then come back when the data is ready.

So, asynchronous programming is not a luxury in JavaScript. It is a design necessity driven by its single-threaded nature.

A more practical async example: fetching data

Timers are one thing, but real asynchronous work usually involves things like network requests.

console.log("Fetching user data...");

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

console.log("This runs before the data arrives");
Enter fullscreen mode Exit fullscreen mode

The output will be:

Fetching user data...
This runs before the data arrives
Got user: Leanne Graham
Enter fullscreen mode Exit fullscreen mode

The fetch call initiates a network request. Instead of blocking the whole script until the server replies, JavaScript hands the work to the browser's networking capabilities (a Web API) and continues executing the rest of the code. Once the response comes back, the callback inside .then is placed in a queue and eventually executed.

How does JavaScript handle async tasks behind the scenes?

This is where a little bit of mental imagery helps. Picture four main pieces:

  1. Call Stack – where synchronous code runs, line by line. Like a stack of plates, the topmost function executes first.

  2. Web APIs (in the browser) or background threads (in Node.js) – where actual long-running tasks live (timers, network requests, DOM events). These are provided by the environment, not by JavaScript itself.

  3. Callback Queue (Task Queue/Macrotask Queue) – where callbacks from async operations like setTimeout, DOM events, and I/O wait to be executed.

  4. Microtask Queue – a special queue with higher priority than the callback queue. It holds promise callbacks (.then, .catch, .finally) and queueMicrotask callbacks. The event loop processes all microtasks before moving to the next macrotask.

And there is the most important piece: the Event Loop. Its job is simple. It constantly checks if the call stack is empty. If it is, it first processes all microtasks in the microtask queue. Only after the microtask queue is empty does it take the next task from the callback queue and push it onto the stack for execution.

When you call setTimeout(callback, 2000), the timer is handed over to a Web API. The main thread moves on. After 2 seconds, the callback goes into the callback queue. The event loop, seeing the empty call stack, first checks the microtask queue. If that’s empty, it picks up the callback from the callback queue and runs it.

This is why even a setTimeout(fn, 0) does not execute immediately. It gets placed into the callback queue and has to wait for the stack to clear and for any microtasks to finish first.

Visualising the flow

If you imagine a timeline for synchronous code, it looks like a straight line where each block of code occupies the full attention of the engine until it's done. No other work happens in between.

For asynchronous code, the timeline looks more like a series of requests sent off to the side, while the main line keeps moving forward. The side tasks eventually finish and silently join the back of a waiting line, ready to be picked up when the main line is free.

When the call stack empties, the event loop pushes the first item from the queue onto the stack. This loop runs continuously, allowing JavaScript to handle many tasks without ever blocking.

Common pitfalls with synchronous thinking

When first learning JavaScript, it is natural to write code assuming everything runs in order. That leads to bugs like:

let userData;
fetch("/api/user")
  .then(res => res.json())
  .then(data => { userData = data; });

console.log(userData); // undefined
Enter fullscreen mode Exit fullscreen mode

The console.log runs before the data has arrived. Fixing this requires placing any dependent code inside the async callback or using promises/async-await semantics, which we will explore in another blog.

Another pitfall is deeply nested callbacks, often called callback hell. We won't dive into that now, but it's important to know that modern JavaScript solves this with Promises and the async/await syntax, making async code look almost synchronous while keeping its non-blocking nature.

Synchronous vs asynchronous: a quick comparison

Synchronous Asynchronous
Code runs one line after another, in order Code can start a task and move on before it finishes
Each operation blocks the thread until completed Operations are non-blocking, allowing the thread to stay responsive
Easy to read and predict, but can cause freezes Needs special handling (callbacks, promises, async/await) but keeps apps smooth
Good for simple calculations, data transformations Essential for network calls, file reading, timers, user events

Key takeaway

The secret to understanding sync vs async lies in accepting that JavaScript is a single-threaded language that offloads lengthy tasks to the environment and picks up the results later. It is like a chef who takes multiple orders and puts them in the oven at different times, rather than staring at the first dish until it's cooked.

Once you embrace this mental model, setTimeout delays, fetch behaviour, and event handling all become predictable. You stop fighting the language and start working with its natural flow.

Conclusion

To sum it up:

  • Synchronous code executes sequentially, blocking further execution until the current operation finishes.
  • Asynchronous code allows the program to start a task and proceed without waiting, keeping the user interface or server responsive.
  • JavaScript needs async behaviour because it runs on a single thread, and blocking that thread would freeze everything.
  • setTimeout, API calls with fetch, and event listeners are common examples of asynchronous operations.
  • Behind the scenes, the event loop, Web APIs, and the callback queue work together to manage async tasks without blocking the call stack.

I hope this gives you a solid foundation. The next time you see a setTimeout execute later than expected or a fetch returning data after a console.log, you will know exactly why.


Hope you found this helpful! If you spot any mistakes or have suggestions, let me know. You can find me on LinkedIn and X, where I post more about web development.

Top comments (0)