Skip to main content

Command Palette

Search for a command to run...

Why Synchronous Systems Break — And How Asynchronous Flow Actually Works (Part 1)

Updated
•6 min read•View as Markdown
Why Synchronous Systems Break — And How Asynchronous Flow Actually Works
 (Part 1)
S

I write about software development, modern tools, and real-world projects. I explain complex concepts in simple language so beginners and intermediate learners can grow confidently.

Have you ever noticed how some websites respond instantly, while others make you wait?

At first, it feels like a performance issue. But most of the time, the real problem isn’t speed. It’s waiting.
And to understand that waiting, we need to understand how work flows inside an application.


The Synchronous Flow (Linear Architecture)

Before we talk about asynchronous systems, let’s visualize how synchronous execution works. In a purely synchronous model, the flow looks like this:
User Action → Task A → Task B → Task C → Response

Each step must complete before the next one begins. There is no skipping. There is no overlap. There is no delegation. If Task B takes 5 seconds, everything behind it waits 5 seconds.

This is called a blocking flow.


Where the Real Problem Happens

Now let’s say Task B is an API call or database query. That operation involves I/O (input/output). And I/O operations are slow compared to CPU execution.

In a blocking architecture:

  • The main thread pauses

  • No other work is processed

  • The system capacity drops

Even if your CPU is powerful, it becomes idle while waiting for external operations.

The bottleneck is not computation. It is waiting.


Blocking vs Non-Blocking (Flow Comparison)

Let’s visually compare the two models.

Blocking Flow

Start Task → Wait → Continue → Wait → Finish

Everything is chained in a single pipeline.

Non-Blocking Flow

Start Task → Delegate → Continue Other Work → Resume When Ready

Now the flow changes. Instead of stopping the main execution, the long-running task is handled separately.


The Real Architecture Behind Asynchronous JavaScript

Now comes the most misunderstood part — the Event Loop.

Many people hear terms like “single-threaded” and immediately get confused. Let’s slow down and build this step by step. JavaScript is single-threaded. That means there is only one main execution thread. Only one piece of JavaScript code can run at a time.

There aren’t multiple JavaScript threads running your code simultaneously. So the natural question is: If there’s only one thread… how does JavaScript handle long tasks like API calls or timers without freezing everything? The answer lies in the architecture around the JavaScript engine.

It’s not just “JavaScript running alone.”
It works together with the browser.

And that architecture looks like this:

  • Call Stack

  • Web APIs

  • Callback Queue

  • Event Loop

Let’s understand each part properly.


The Call Stack — Where Your Code Runs

The Call Stack is where JavaScript executes your code.

When a function runs, it gets pushed onto the stack. When it finishes, it gets popped off. It works in a LIFO manner.

For example:

function greet() {
  console.log("Hello");
}
greet();

When greet() runs:

  • It gets pushed onto the call stack.

  • console.log() runs.

  • Then the function is removed from the stack.

As long as the code is synchronous, everything happens directly inside this call stack. But the moment JavaScript encounters something like:

setTimeout(() => {
  console.log("Done");
}, 2000);

It behaves differently.


Web APIs — Where Long Tasks Are Handled

When JavaScript sees setTimeout() or fetch(), it does not handle them inside the Call Stack.

Instead, it delegates them to the browser’s Web APIs. Web APIs are provided by the browser (or Node.js runtime). They handle:

  • Timers (setTimeout, setInterval)

  • Network requests (fetch)

  • DOM events

So when you write:

setTimeout(() => {
  console.log("Done");
}, 2000);

Here’s what happens:

  1. setTimeout() is pushed onto the Call Stack.

  2. JavaScript tells the Web API: “Wait 2 seconds, then run this function.”

  3. setTimeout() is removed from the Call Stack.

  4. The Call Stack continues executing the next line of code.

The key point:

The timer runs in the background — not inside the Call Stack. That’s how JavaScript avoids blocking.


Callback Queue — Waiting Area for Completed Tasks

Now let’s say the 2 seconds are over. The Web API says: “Okay, the timer is done.”
But it cannot directly push the callback into the Call Stack.

Why?

Because the Call Stack might still be busy executing other code. So instead, the completed task is placed into something called the Callback Queue. Think of the Callback Queue as a waiting room. All completed asynchronous tasks wait there until JavaScript is ready to execute them.


The Event Loop — The Traffic Controller

Now comes the most important piece: the Event Loop. The Event Loop constantly monitors two things:

  • Is the Call Stack empty?

  • Is there something waiting in the Callback Queue?

If the Call Stack is empty, the Event Loop takes the first task from the Callback Queue and pushes it onto the Call Stack. Then that task executes.

If the Call Stack is not empty, the Event Loop waits. It does not interrupt running code. It does not force parallel execution. It simply waits for the right moment to move tasks.

That’s it.


Let’s Visualize the Full Flow

When you run this code:

console.log("Start");

setTimeout(() => {
  console.log("Inside Timeout");
}, 2000);

console.log("End");

Here’s what happens step by step:

  1. "Start" is printed.

  2. setTimeout() is delegated to Web APIs.

  3. "End" is printed.

  4. After 2 seconds, the callback moves to the Callback Queue.

  5. The Event Loop waits for the Call Stack to be empty.

  6. The callback is pushed to the Call Stack.

  7. "Inside Timeout" is printed.

Output:

Start
End
Inside Timeout

Even though the timeout was written before "End" in the code. This is asynchronous flow in action.


async / await in Architecture Context

When you write:

const response = await fetch(url);

It looks like JavaScript stops at that line and waits. But it doesn’t block the entire system. Behind the scenes, the same architecture is still working:

Call Stack → Web API → Queue → Event Loop → Call Stack

Here’s what actually happens:

  • fetch(url) is sent to the Web API.

  • It immediately returns a Promise.

  • await pauses only this function — not the whole program.

  • The rest of JavaScript can continue running.

  • When the request finishes, the result goes into a queue.

  • The Event Loop moves it back to the Call Stack when it’s free.

  • The paused function then resumes.

So nothing about the architecture changes. async/await does not make JavaScript synchronous. It only makes asynchronous code easier to read. Under the hood, it still uses Promises and the Event Loop.


The Core Mental Model of Part 1

At this point, you should visually understand:

  • Synchronous = linear blocking pipeline

  • Asynchronous = delegated managed flow

  • Event loop = traffic manager

  • Promises = future value contract

  • async/await = readability layer

And all of it exists for one reason: To prevent waiting from freezing the system.


Smooth Bridge to Part 2

Everything we discussed so far happens inside one runtime. But what happens when:

Service A waits for Service B?
And Service B waits for Service C?

What if entire systems start blocking each other? That’s when asynchronous flow moves from programming… Into architecture between services.

And that’s where messaging, queues, and event-driven systems come in.


About the Author

Shrey Joshi - Breaking down complex concepts with clarity and context.

LinkedIn | Medium | Instagram
Building learn.with.Shrey

More from this blog

learn.with.Shrey

17 posts

Breaking down complex concepts with clarity and context.