At its core, JavaScript is a synchronous, blocking and single-threaded programming language. This means the JavaScript engine has only one call stack and executes your code sequentially – one line at a time, in order. If a specific task takes a long time to complete, it will block the execution of any code following it.
However, JavaScript can behave asynchronously because of the runtime environment (like a web browser or Node.js) that surrounds it.
How JavaScript Handles Asynchronicity
While the JavaScript language itself cannot do two things at once, it offloads time-consuming tasks to the environment:
| Component | Role in the Ecosystem |
|---|---|
| JavaScript Engine | Synchronous. Handles the single call stack and executes the actual code instructions. |
| Runtime Environment (Browser/Node) | Asynchronous. Provides background tools like Web APIs (e.g., fetch(), setTimeout) to handle long operations without freezing the main thread. |
| The Event Loop | The Coordinator. Constantly watches the call stack. Once the synchronous code finishes and the stack is empty, it pushes the completed asynchronous callbacks back into the stack to be executed. |
Direct Comparison
// 1. SYNCHRONOUS EXAMPLE
console.log("First");
console.log("Second"); // "Second" absolutely waits for "First" to finish
// 2. ASYNCHRONOUS EXAMPLE
console.log("First");
setTimeout(() => {
console.log("Second"); // Sent to a browser timer API; does not block the next line
}, 1000);
console.log("Third");
// Output:
// First
// Third
// Second
By using native features like Callbacks, Promises and async/await, developers write code that handles modern web requirements – like loading data from servers – without freezing user interfaces
The key misconception is this:
JavaScript does NOT execute normal function calls asynchronously.
Your code is completely synchronous.
So when JS reaches:
outer();
it must finish executing outer() before moving to the next line:
console.log(name);
That’s why the output is:
JaneJohnAbhilash
1. Let’s execute it step by step
Your code:
const name = "Abhilash";
function outer() {
const name = "John";
if (true) {
const name = "Jane";
console.log(name);
}
console.log(name);
}
outer();
console.log(name);
JavaScript starts at the top:
const name = "Abhilash";
Global name is:
name = "Abhilash"
Then JS encounters the function declaration:
function outer() { ... }
The function is defined, but its body is not executed yet.
Then:
outer();
This means:
Call
outer()and execute its body now.
2. What happens inside outer()?
First:
const name = "John";
Now there is a new local variable:
Global scope
name = "Abhilash"
outer() scope
name = "John"
Then:
if (true) {
const name = "Jane";
console.log(name);
}
The if block creates another scope:
Global scope
name = "Abhilash"
outer scope
name = "John"
if block scope
name = "Jane"
So:
console.log(name);
looks for name in the nearest scope.
It finds:
Jane
So:
Jane
is printed.
3. After the if block ends
The block-scoped variable:
const name = "Jane";
is no longer accessible.
We’re back in the outer() function’s scope:
outer scopename = "John"
So:
console.log(name);
prints:
John
Then outer() finishes.
Only now does JavaScript continue to:
console.log(name);
We’re back in the global scope:
name = "Abhilash"
So it prints:
Abhilash
Therefore:
Jane
John
Abhilash
4. Why doesn’t JavaScript “move fast” to the next line?
This is the important part.
You said:
“because JavaScript has asynchronous execution”
That isn’t quite correct.
JavaScript supports asynchronous operations, but ordinary JavaScript execution is synchronous.
Consider:
function hello() {
console.log("Hello");
}
hello();
console.log("World");
The output is:
HelloWorld
5. Think about the call stack
This is the easiest mental model for interviews.
When JS sees:
outer();
the function is pushed onto the call stack.
Call Stack
outer()
------
global code
Then JavaScript executes:
const name = "John";
Then the if block:
console.log(name);
prints:
Jane
Then:
console.log(name);
prints:
John
Then outer() finishes and is removed from the stack:
Call Stack
global code
Now JavaScript executes:
console.log(name);
and prints:
Abhilash
6. Where does asynchronous behavior actually happen?
Consider:
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
console.log("C");
Output:
ACB
Here you get asynchronous behavior.
Why?
console.log("A")
↓
A
setTimeout(...)
↓
schedule callback
console.log("C")
↓
C
current synchronous code finishes
↓
event loop
↓
callback runs
↓
B
So:
Normal function call
↓
Synchronous
setTimeout / Promise / fetch / async I/O
↓
Can involve asynchronous execution
7. fetch() is a good example related to your React questions
console.log("A");
fetch("/api/users")
.then(() => {
console.log("B");
});
console.log("C");
You’ll normally see:
ACB
fetch() starts an asynchronous operation. JavaScript doesn’t wait for the network request to finish before continuing with:
console.log("C");
That’s different from:
outer();
console.log(name);
because outer() is just an ordinary synchronous function.
8. Your outer() example is also demonstrating lexical scoping
There are three different name variables:
const name = "Abhilash"; // global
function outer() {
const name = "John"; // outer scope
if (true) {
const name = "Jane"; // block scope
}
}
They’re not the same variable.
The inner declarations shadow the outer declarations.
JavaScript resolves:
console.log(name);
by looking outward:
Current scope
↓
Parent scope
↓
Parent scope
↓
Global scope
So inside the if:
Jane found
Inside outer():
John found
Outside outer():
Abhilash found
The most important correction to remember
Don’t think:
“JavaScript is asynchronous, so it doesn’t wait for functions.”
Think:
“JavaScript executes normal code synchronously. It waits for an ordinary function call to finish. Asynchronous APIs such as timers, network requests, and Promises allow work to be completed later via the event loop.”
That’s a very important distinction for understanding React, promises, async/await, fetch, callbacks and the JavaScript event loop.