sync, async, setTimeoutZero | Node Js
Episode 07 - sync, async, setTimeoutZero
Hey everyone! Welcome back to the Node.js tutorial series. Let me explain something further about the code we discussed in our last episode.
Today we're going to understand the difference between sync and async operations, and discover the magic of setTimeout(0)!
What we will cover:
- fs.readFileSync - Blocking Code
- Why Blocking is Bad
- Best Practices
- The crypto Module
- pbkdf2Sync vs pbkdf2
- The Magic of setTimeout(0)
- Interview Questions
fs.readFileSync - Blocking the Main Thread
fs.readFileSync("./file.txt", "utf8");
This function reads the contents of a file synchronously, meaning it will actually block the main thread while it's running.
What does this mean?
The V8 engine is responsible for executing JavaScript code, but it cannot offload this task to Libuv (which handles asynchronous operations like I/O tasks).
Synchronous File Read:
======================
Line 1: let a = 10; ✅ Executed
Line 2: let b = 20; ✅ Executed
Line 3: fs.readFileSync(...) ⏳ BLOCKED! Waiting...
(Main thread stuck here)
(Nothing else can run)
...
...File read complete!
Line 4: console.log("Done"); ✅ Now executes
The Ice Cream Shop Analogy 🍦
Think of it like an ice cream shop where the owner insists on serving each customer one at a time, without moving on to the next until the current customer has been fully served.
Ice Cream Shop (Synchronous):
=============================
Customer 1: "I want vanilla!"
Owner: "Let me make it..." (5 minutes)
↓
Customer 2: Waiting... 😤
Customer 3: Waiting... 😤
Customer 4: Waiting... 😤
↓
Owner: "Here's your vanilla!"
↓
Customer 2: "Finally! I want chocolate!"
Owner: "Let me make it..." (5 minutes)
↓
Customer 3: Still waiting... 😡
Customer 4: Still waiting... 😡
This is what happens when you use fs.readFileSync()!
This is what happens when you use synchronous methods like fs.readFileSync() — the main thread is blocked until the file is completely read!
Why is this Important?
As a developer, it's important to understand that while Node.js and the V8 engine give you the capability to block the main thread using synchronous methods, this is generally not recommended.
Synchronous methods like fs.readFileSync() are still available in Node.js, but using them can cause performance issues because the code execution will be halted at that point until the file reading operation is complete.
The Problem: ============ // Imagine 1000 users requesting your server User 1 Request → fs.readFileSync() → BLOCKED for 2 seconds User 2: Waiting... User 3: Waiting... ... User 1000: Waiting... All 1000 users are stuck waiting for User 1's file to be read! This is TERRIBLE for performance!
Best Practice
Avoid using synchronous methods in production code whenever possible, especially in performance-critical applications, because they can slow down your application by blocking the event loop.
Instead, use asynchronous methods like fs.readFile() that allow other operations to continue while the file is being read, keeping the application responsive.
// ❌ BAD - Synchronous (Blocking)
const data = fs.readFileSync("./file.txt", "utf8");
console.log(data);
// ✅ GOOD - Asynchronous (Non-Blocking)
fs.readFile("./file.txt", "utf8", (err, data) => {
console.log(data);
});
// Other code continues to run!
Introducing the crypto Module
Let me show you another example of blocking code in Node.js.
Node.js has a core library known as crypto, which is used for cryptographic operations like generating secure keys, hashing passwords, and more.
What is crypto?
The crypto module is one of the core modules provided by Node.js, similar to other core modules like https, fs (file system), and zlib (used for compressing files).
These core modules are built into Node.js, so when you write require('crypto'), you're importing a module that is already present in Node.js.
// Both ways work:
const crypto = require('crypto');
const crypto = require('node:crypto'); // Explicit core module
You can also import it using require('node:crypto') to explicitly indicate that it's a core module, but this is optional.
Example of Blocking Code with crypto
One of the functions provided by the crypto module is pbkdf2Sync, which stands for Password-Based Key Derivation Function 2.
This function is used to generate a cryptographic key from a password and salt, and it operates synchronously.
const crypto = require('crypto');
// Synchronous version - BLOCKS main thread!
const key = crypto.pbkdf2Sync(
'password', // Password
'salt', // Salt
50000, // Iterations (50,000)
50, // Key length (50 bytes)
'sha512' // Digest algorithm
);
console.log("Key generated!");
Here's what this function does:
- Password and Salt: You provide a password and a salt value, which are combined to create a cryptographic key
- Iterations: You specify the number of iterations (e.g., 50,000) to increase the complexity of the key, making it harder to crack
- Digest Algorithm: You choose a digest algorithm, like sha512, which determines how the key is hashed
- Key Length: You define the length of the key (e.g., 50 bytes)
- Callback: In the asynchronous version (pbkdf2), a callback is provided to handle the result once the key is generated
Important Note: The "Sync" Suffix
When you see Sync at the end of a function name (like pbkdf2Sync), it means that the function is synchronous and will block the main thread while it's running.
This is something you should be cautious about, especially in performance-sensitive applications!
Naming Convention: ================== fs.readFileSync() → Synchronous (Blocking) fs.readFile() → Asynchronous (Non-Blocking) crypto.pbkdf2Sync() → Synchronous (Blocking) crypto.pbkdf2() → Asynchronous (Non-Blocking) See the pattern? "Sync" suffix = Blocking!
Why Does This Matter?
Blocking Behavior: The synchronous version of pbkdf2 (pbkdf2Sync) will block the event loop, preventing any other code from executing until the key generation is complete. This can cause your application to become unresponsive if used inappropriately.
Asynchronous Alternative: Node.js also provides an asynchronous version (pbkdf2 without the Sync suffix), which offloads the operation to Libuv. This allows the event loop to continue processing other tasks while the key is being generated.
const crypto = require('crypto');
console.log("Start");
// ❌ Synchronous - Blocks everything
const key1 = crypto.pbkdf2Sync('password', 'salt', 50000, 50, 'sha512');
console.log("Key 1 generated (sync)");
// ✅ Asynchronous - Non-blocking
crypto.pbkdf2('password', 'salt', 50000, 50, 'sha512', (err, key2) => {
console.log("Key 2 generated (async)");
});
console.log("End");
OUTPUT: Start Key 1 generated (sync) ← First (because it's synchronous) End Key 2 generated (async) ← Last (because it's asynchronous)
The first key is generated first because it's synchronous, while the second key is generated afterward because it's asynchronous!
New Interesting Concept: setTimeout(0) ⏱️
Now let's learn about something very interesting!
Interview Tip: This can be a tricky output question!
Q: What will be the output of the following code?
console.log("Start");
setTimeout(() => {
console.log("Inside setTimeout");
}, 0);
console.log("End");
Think about it... 🤔
The delay is 0 milliseconds, so shouldn't "Inside setTimeout" print immediately after "Start"?
Answer:
OUTPUT: Start End Inside setTimeout
Wait, what?! Even though we set the delay to 0 milliseconds, the setTimeout callback runs LAST!
Why is setTimeout(0) Executed After the Multiplication Result?
You might wonder why the setTimeout(0) callback is executed after other code, even though we set the delay to 0 milliseconds.
The Reason:
Asynchronous Operation: setTimeout is an asynchronous function, meaning it doesn't block the execution of the code. When you call setTimeout, even with 0ms delay, it's still handed over to libuv!
The setTimeout(0) Process:
==========================
1. console.log("Start") → Executes immediately (sync)
2. setTimeout(..., 0) → V8 says "This is async!"
→ Hands it to libuv
→ libuv registers callback
→ V8 continues to next line
3. console.log("End") → Executes immediately (sync)
4. Call stack is now EMPTY!
5. Event loop checks: "Any callbacks ready?"
→ Yes! setTimeout callback is ready
→ Pushes callback to call stack
6. console.log("Inside setTimeout") → Executes
The Key Insight
setTimeout(0) doesn't mean "execute immediately"!
It means: "Execute this callback as soon as possible, but AFTER all synchronous code has finished and the call stack is empty."
Think of it like this: ====================== Synchronous Code = VIP Queue (First Priority) Asynchronous Callbacks = Regular Queue (Second Priority) Even if async callback has 0ms delay, it still has to wait for all VIPs (sync code) to finish!
More Examples
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
setTimeout(() => {
console.log("3");
}, 0);
console.log("4");
OUTPUT: 1 4 2 3
Explanation:
- "1" - Synchronous, executes immediately
- setTimeout "2" - Async, registered with libuv
- setTimeout "3" - Async, registered with libuv
- "4" - Synchronous, executes immediately
- Call stack empty - Event loop runs callbacks
- "2" - First setTimeout callback
- "3" - Second setTimeout callback
Another Tricky Example
console.log("Start");
setTimeout(() => {
console.log("Timeout 1");
}, 0);
Promise.resolve().then(() => {
console.log("Promise 1");
});
setTimeout(() => {
console.log("Timeout 2");
}, 0);
Promise.resolve().then(() => {
console.log("Promise 2");
});
console.log("End");
OUTPUT: Start End Promise 1 Promise 2 Timeout 1 Timeout 2
Why? Promises have higher priority than setTimeout! (We'll learn more about this in the Event Loop episode)
Practical Use Case of setTimeout(0)
When would you actually use setTimeout(0)?
// Use case: Defer execution until call stack is clear
console.log("Processing heavy data...");
// This defers the callback to run after current execution
setTimeout(() => {
console.log("UI is now responsive!");
// Do something that can wait
}, 0);
console.log("Continuing with important stuff...");
// Output:
// Processing heavy data...
// Continuing with important stuff...
// UI is now responsive!
This pattern is useful when you want to ensure some code runs after the current synchronous execution completes!
Quick Recap
| Concept | Description |
|---|---|
| Sync Methods | Block main thread (e.g., readFileSync) |
| Async Methods | Non-blocking, offloaded to libuv |
| "Sync" Suffix | Indicates synchronous/blocking function |
| crypto Module | Core module for cryptographic operations |
| setTimeout(0) | Runs after all sync code completes |
| Best Practice | Avoid sync methods in production |
Interview Questions
Q: What is the difference between fs.readFileSync and fs.readFile?
"fs.readFileSync is synchronous and blocks the main thread until the file is completely read. fs.readFile is asynchronous and non-blocking - it offloads the operation to libuv and allows other code to continue executing."
Q: Why should we avoid synchronous methods in production?
"Synchronous methods block the event loop, preventing any other code from executing. In a server handling multiple requests, this means all users would have to wait for one operation to complete, causing severe performance issues."
Q: What will be the output of console.log("A"); setTimeout(() => console.log("B"), 0); console.log("C");?
"The output will be A, C, B. Even though setTimeout has 0ms delay, it's still asynchronous. The callback is registered with libuv and only executes after all synchronous code (A and C) has finished."
Q: What does the "Sync" suffix mean in Node.js function names?
"The 'Sync' suffix indicates that the function is synchronous and will block the main thread while executing. Examples include fs.readFileSync, crypto.pbkdf2Sync, etc."
Q: What is the purpose of setTimeout(0)?
"setTimeout(0) is used to defer execution of a callback until after the current call stack is clear. It doesn't mean 'execute immediately' - it means 'execute as soon as possible after all synchronous code has finished.'"
Key Points to Remember
- Sync methods block the main thread
- Async methods are non-blocking (use libuv)
- Look for "Sync" suffix to identify blocking functions
- Avoid sync methods in production/performance-critical code
- crypto is a core module for cryptography
- setTimeout(0) doesn't mean "execute now"
- Async callbacks run after all sync code
- Event loop only runs callbacks when call stack is empty
- Promises have higher priority than setTimeout
What's Next?
Now you understand the difference between sync and async operations, and the magic of setTimeout(0)! In the next episode, we will:
- Deep dive into the Event Loop
- Understand phases of the Event Loop
- Learn about microtasks and macrotasks
Keep coding, keep learning! See you in the next one!
Post a Comment