The Foundation — JavaScript Is Synchronous & Single-Threaded
Before we can understand how Node.js handles async operations, we need to be crystal clear about what JavaScript actually is at its core.
Three properties of JavaScript matter here:
- Single-threaded. JavaScript runs on one thread with a single call stack. At any given moment, only one piece of code is being processed. There is no parallelism inside the JS engine itself.
- Synchronous. Code executes line by line, in order. Line 2 only runs after Line 1 finishes completely. If a function takes time, it blocks everything after it.
- V8 engine. The V8 engine (used in Chrome and Node.js) runs on this single thread, parsing and executing JavaScript sequentially. It manages the call stack, memory heap, and garbage collection.
In languages like C++ or Java, code can run across multiple threads simultaneously — one part on Thread 1, another on Thread 2. JavaScript doesn’t do this. It executes one line, then the next, then the next. If you’re executing line 2, it will only run after line 1 has completely finished. That is the essence of synchronous execution.
Synchronous vs Asynchronous — The Ice Cream Shop
| Axis | Synchronous | Asynchronous |
|---|---|---|
| Workers | One hand, N tasks | N hands, N tasks |
| Ordering | Strictly sequential | Independent, parallel |
| Total time | Sum of all tasks | Longest single task |
| Behaviour | Later tasks wait | No task waits for another |
Here are two minimal code examples that show the split:
let a = 10786;
let b = 20987;
function multiplyFn(x, y) {
const result = x * y;
return result;
}
let c = multiplyFn(a, b);
console.log(c); // 226215682// 1. API Call — network request, takes time
https.get("https://api.fbi.com", (res) => {
console.log("secret data:", res.secret);
});
// 2. File Read — disk I/O, takes time
fs.readFile("./gossip.txt", "utf8", (data) => {
console.log("File Data", data);
});
// 3. Timer — waits 5 seconds
setTimeout(() => {
console.log("wait here for 5 seconds");
}, 5000);The synchronous code executes immediately — plain math, variable assignment, function calls. The asynchronous code involves waiting: for a network response, for a file to be read from disk, for a timer to expire. These are the operations that Node.js offloads to libuv.
Inside V8 — Call Stack, Memory Heap & Garbage Collector
The V8 engine has three key components that work together to run your JavaScript:
- Call stack (LIFO). A stack data structure where function calls are pushed and popped. The currently executing function sits on top. Only one function runs at a time — this is why JS is single-threaded.
- Memory heap. An unstructured pool of memory where variables, objects, closures, and function definitions are stored. This is where your data lives while the program runs.
- Garbage collector. Automatically identifies variables and objects that are no longer referenced and frees their memory. Unlike C++, you never manage memory manually in JavaScript.
┌─────────────────────────────────────────────────────────┐
│ V8 JS ENGINE (Single Thread) │
│ │
│ ┌──────────────┐ ┌──────────────────┐ ┌──────────┐ │
│ │ Call Stack │ │ Memory Heap │ │ Garbage │ │
│ │ (LIFO) │ │ (objects, vars, │ │Collector │ │
│ │ │ │ closures, fns) │ │ │ │
│ │ multiplyFn() │ │ │ │ Finds & │ │
│ │ ──────────── │ │ a = 10786 │ │ frees │ │
│ │ GEC │ │ b = 20987 │ │ unused │ │
│ │ │ │ multiplyFn=fn{} │ │ memory │ │
│ └──────────────┘ └──────────────────┘ └──────────┘ │
│ ↑ │
│ Executes code line by line │
└─────────────────────────────────────────────────────────┘Key insight: when you write synchronous code, it all happens step by step in the call stack, using memory from the heap, and the garbage collector quietly tidies up behind the scenes. No interruptions, no parallel tasks — just one thing at a time.
How Synchronous Code Executes — Step by Step
Let’s trace exactly how V8 executes this synchronous code through the call stack:
let a = 10786; // Line 1
let b = 20987; // Line 2
function multiplyFn(x, y) { // Line 4
const result = x * y; // Line 5
return result; // Line 6
} // Line 7
let c = multiplyFn(a, b); // Line 9
console.log(c); // Line 10 → outputs 226215682- Global Execution Context (GEC) created. As soon as V8 begins, it
creates the GEC — the main environment for top-level code. This is
always the first thing pushed onto the call stack. The stack now has:
[GEC]. - Memory creation phase (hoisting). Before any code runs, V8 scans
the entire file. Variables
aandbare allocated in memory and set toundefined. The functionmultiplyFnis stored in memory with its entire function definition — this is why function declarations can be called before they appear in code. - Code execution phase begins.
let a = 10786executes:ais now assigned10786. Thenlet b = 20987executes:bgets20987. V8 processes these line by line, top to bottom. The function definition on line 4 is skipped (already stored in memory). - Function invocation — new execution context. When
multiplyFn(a, b)is called on line 9, V8 creates a new Function Execution Context (FEC) and pushes it onto the top of the call stack. Parametersxandyreceive the values ofa(10786) andb(20987). Call stack:[FEC(multiplyFn), GEC]. - Execute inside multiplyFn. Inside the function,
resultis initialized asundefined(memory phase), then assigned10786 × 20987 = 226215682(execution phase). Thenreturn resultis hit — the function returns226215682and its execution context is popped off the call stack. The garbage collector may now clean up memory used by this context. - Resume GEC — assign return value. Back in the GEC, the returned
value
226215682is assigned to variablec. Thenconsole.log(c)prints the result to the console. Call stack:[GEC]. - GEC popped — call stack empty. All code has been executed. The GEC is removed from the call stack. The stack is now completely empty, and the JS engine is idle. Program ends.
The Superhero — libuv & How Async Code Executes
Here’s the million-dollar question: if V8 is single-threaded and synchronous, how does Node.js handle things like API calls, file reads, and timers without blocking everything?
The answer: V8 doesn’t do it alone. The JavaScript engine has no concept of time or waiting. It needs superpowers. This is where Node.js comes in, and it grants those powers through a C library called libuv.
┌──────────────────┐ ┌──────────┐ ┌──────────────┐
│ V8 ENGINE │ │ LIBUV │ │ OS │
│ │ async │ │ talks │ │
│ Call Stack │ ──────→ │ Event │ ──────→ │ Files │
│ Memory Heap │ offload │ Loop │ to │ Network │
│ Garbage │ │ Thread │ │ Database │
│ Collector │ ←────── │ Pool │ ←────── │ Timers │
│ │ callback │ │ results │ │
└──────────────────┘ └──────────┘ └──────────────┘
JS code runs here Manages async System-level
synchronously operations resourcesComplete Async Walkthrough — The Big Picture
Let’s trace through a mixed sync + async program to see how V8 and libuv work together:
let a = 10786;
let b = 20987;
// Async operation 1: API call (callback A)
https.get("https://api.fbi.com", (res) => { // ← Callback A
console.log(res.secret);
});
// Async operation 2: Timer (callback B)
setTimeout(() => { // ← Callback B
console.log("setTimeout");
}, 5000);
// Async operation 3: File read (callback C)
fs.readFile("./gossip.txt", "utf8", (data) => { // ← Callback C
console.log("File Data", data);
});
// Sync: function definition + call
function multiplyFn(x, y) {
const result = x * y;
return result;
}
let c = multiplyFn(a, b);
console.log(c); // 226215682Now line by line — here’s where each one goes:
- GEC created —
aandbassigned. The twoletlines execute synchronously in the call stack within the GEC. Simple variable assignments — V8 handles them directly. https.get()hit — async, offloaded to libuv. V8 encounters the API call and recognizes it as asynchronous. It signals libuv to handle it. libuv registers the API call with its associated callback A in its event loop. V8 does not wait — it immediately moves on.setTimeout()hit — async, offloaded to libuv. V8 sees another async operation. It hands the timer off to libuv with callback B. libuv starts tracking the 5-second timer. V8 keeps going.fs.readFile()hit — async, offloaded to libuv. Yet another async operation. V8 offloads the file read to libuv with callback C. libuv uses its thread pool to perform the file system operation in the background. V8 continues without waiting.multiplyFn(a, b)— sync, runs on call stack. V8 encounters the function call. A new Function Execution Context is created and pushed onto the call stack.xgets 10786,ygets 20987.result = 10786 * 20987 = 226215682. The function returns, its context is popped, andcreceives the value.console.log(c)— prints 226215682. This is the last synchronous line. The GEC is popped off the call stack. The call stack is empty. V8 is idle.- libuv takes over — heavy lifting in the background. While V8
relaxes, libuv is busy: processing the 5-second timer, waiting for the
network response from the API, and reading
gossip.txtfrom disk. It handles all these operations by communicating with the OS. - Async operations complete — callbacks return via event loop. As each async operation finishes, libuv pushes the corresponding callback onto the callback queue. The event loop checks: “Is the call stack empty?” If yes, it moves the callback from the queue onto the call stack, and V8 executes it.
- Callbacks execute one by one. Callback C (file read) might finish first — it runs. Then Callback A (API response) comes back — it runs. Finally Callback B (timer) fires after 5 seconds — it runs. Each callback is executed one at a time on the single-threaded call stack, but the waiting happened in parallel via libuv.
The Event Loop & Callback Queue
One final layer connects the two halves: the event loop and its task queues.
┌─────────────────────────────────────────────────────────┐
│ Main Thread (V8) │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Call Stack │ │ Memory Heap │ │
│ │ (LIFO) │ │ (objects, closures) │ │
│ └──────────────┘ └──────────────────────┘ │
│ ↑ │
│ Executes code line by line │
└─────────────────────────────────────────────────────────┘
│
│ Async operations handed off to Web APIs / Node APIs
▼
┌─────────────────────────────────────────────────────────┐
│ Event Loop & Task Queues │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Macrotask Queue │ │ Microtask Queue │ │
│ │ (setTimeout, │ │ (Promises, │ │
│ │ setInterval, │ │ queueMicrotask, │ │
│ │ I/O callbacks) │ │ MutationObserver)│ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ Event loop checks: "Is call stack empty?" │
│ If YES → move next callback to call stack │
│ Microtasks always run before the next macrotask │
└─────────────────────────────────────────────────────────┘The event loop follows a simple but powerful rule: it continuously checks
whether the call stack is empty. If it is, it picks the next callback
from the queue and pushes it onto the call stack for V8 to execute.
Microtasks (like Promise.then callbacks) always have priority over
macrotasks (like setTimeout callbacks).
Wherever V8 encounters an asynchronous operation, it offloads it to
libuv. libuv does the further processing, and once the operation is
complete, it returns the result to V8 through the event loop. V8 then
quickly executes the callback. Examples of async operations: API calls,
setTimeout, file read/write, database queries, DNS lookups.
Episode 06 — At a Glance
| Concept | Key detail |
|---|---|
| Thread | Smallest unit of execution in a process; JS uses a single thread. |
| Synchronous | Code runs line by line; each operation blocks the next until complete. |
| Asynchronous | Tasks run independently; no waiting for others to finish. |
| JS nature | Synchronous + single-threaded — but Node.js adds async powers. |
| Call stack | LIFO stack where function calls are pushed/popped; one at a time. |
| Memory heap | Stores variables, objects, functions — unstructured memory pool. |
| Garbage collector | Auto-frees unused memory; no manual deallocation needed (unlike C++). |
| GEC | Global Execution Context — first thing pushed on the call stack. |
| Execution phases | Memory creation (vars = undefined, fns stored) → code execution (assign + run). |
| libuv | C library giving Node.js async powers: event loop, thread pool, OS communication. |
| Async flow | V8 offloads → libuv handles in background → callbacks return via event loop. |
| Non-blocking I/O | Main thread never waits; async work happens in libuv while V8 continues. |
| Event loop | Continuously checks if call stack is empty, then moves callbacks from queue. |
| Microtasks vs macrotasks | Promises (microtask) always execute before setTimeout (macrotask) callbacks. |
Comments
Comments are disabled in this environment. Set
PUBLIC_GISCUS_REPO,PUBLIC_GISCUS_REPO_ID, andPUBLIC_GISCUS_CATEGORY_IDto enable.