HTML & JS Topic
Node.js Asynchronous JavaScript
Understanding Asynchronous Programming in Node.js
Node.js is single-threaded but handles thousands of concurrent connections through non-blocking I/O and event-driven architecture.
Why Asynchronous?
-
Non-blocking: Don't wait for I/O operations to complete
-
High concurrency: Handle multiple operations simultaneously
-
Better performance: Efficient use of single thread
-
Scalability: Handle many connections with minimal resources
1. Callbacks
Callbacks are functions passed as arguments to other functions, to be executed later when an operation completes.
How Callbacks Work:
javascript
// Synchronous callback (executed immediately)
function syncCallback(callback) {
console.log('Before callback');
callback();
console.log('After callback');
}
// Asynchronous callback (executed later)
function asyncCallback(callback) {
console.log('Before callback');
setTimeout(callback, 1000); // Execute after 1 second
console.log('After callback');
}
syncCallback(() => console.log('Callback executed'));
// Output: Before callback → Callback executed → After callback
asyncCallback(() => console.log('Async callback executed'));
// Output: Before callback → After callback → (1 sec later) Async callback executed
Node.js Style Callbacks (Error-First Pattern):
Node.js convention: Callbacks take (error, result) as parameters.
javascript
const fs = require('fs');
// Read file with callback
fs.readFile('file.txt', 'utf8', (error, data) => {
if (error) {
console.error('Error reading file:', error);
return;
}
console.log('File content:', data);
});
// Custom function with Node.js callback pattern
function divide(a, b, callback) {
if (b === 0) {
// Error-first: pass error as first argument
callback(new Error('Cannot divide by zero'), null);
} else {
// Success: error is null, result is second argument
callback(null, a / b);
}
}
// Using the function
divide(10, 2, (error, result) => {
if (error) {
console.error('Error:', error.message);
} else {
console.log('Result:', result); // 5
}
});
Callback Hell (Pyramid of Doom):
javascript
// Nested callbacks become hard to read and maintain
fs.readFile('file1.txt', 'utf8', (err1, data1) => {
if (err1) {
console.error(err1);
return;
}
fs.readFile('file2.txt', 'utf8', (err2, data2) => {
if (err2) {
console.error(err2);
return;
}
fs.writeFile('combined.txt', data1 + data2, (err3) => {
if (err3) {
console.error(err3);
return;
}
console.log('Files combined successfully');
});
});
});
2. Promises
Promises represent the eventual completion (or failure) of an asynchronous operation and its resulting value.
Promise States:
-
Pending: Initial state, neither fulfilled nor rejected
-
Fulfilled: Operation completed successfully
-
Rejected: Operation failed
Creating Promises:
javascript
// Create a promise
const myPromise = new Promise((resolve, reject) => {
// Asynchronous operation
const success = true; // Simulate success/failure
setTimeout(() => {
if (success) {
resolve('Operation successful!'); // Fulfill promise
} else {
reject(new Error('Operation failed!')); // Reject promise
}
}, 1000);
});
Using Promises:
javascript
// Consuming a promise
myPromise
.then((result) => {
console.log('Success:', result);
return result.toUpperCase(); // Can return value for next .then()
})
.then((modifiedResult) => {
console.log('Modified:', modifiedResult);
})
.catch((error) => {
console.error('Error:', error.message);
})
.finally(() => {
console.log('Operation completed (success or failure)');
});
Promise Methods:
1. Promise.resolve() & Promise.reject():
javascript
// Create already resolved/rejected promises
const resolvedPromise = Promise.resolve('Immediate value');
const rejectedPromise = Promise.reject(new Error('Immediate error'));
2. Promise.all(): Wait for all promises to resolve
javascript
const promise1 = Promise.resolve(3);
const promise2 = 42; // Non-promise values are converted
const promise3 = new Promise((resolve) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log(values); // [3, 42, 'foo'] after 100ms
})
.catch((error) => {
// If ANY promise rejects, entire Promise.all rejects
});
3. Promise.allSettled(): Wait for all promises to settle (resolve or reject)
javascript
Promise.allSettled([
Promise.resolve(1),
Promise.reject(new Error('Error')),
Promise.resolve(3)
]).then((results) => {
results.forEach((result) => {
console.log(result.status); // "fulfilled" or "rejected"
});
});
4. Promise.race(): Returns first settled promise (resolve or reject)
javascript
const fastPromise = new Promise((resolve) => {
setTimeout(resolve, 100, 'Fast');
});
const slowPromise = new Promise((resolve) => {
setTimeout(resolve, 500, 'Slow');
});
Promise.race([fastPromise, slowPromise])
.then((value) => {
console.log(value); // "Fast" (after 100ms)
});
5. Promise.any(): Returns first fulfilled promise (ignores rejections)
javascript
const rejectedPromise = Promise.reject(new Error('Fail'));
const slowPromise = new Promise((resolve) => {
setTimeout(resolve, 500, 'Slow');
});
const fastPromise = new Promise((resolve) => {
setTimeout(resolve, 100, 'Fast');
});
Promise.any([rejectedPromise, slowPromise, fastPromise])
.then((value) => {
console.log(value); // "Fast"
});
Converting Callbacks to Promises:
javascript
const fs = require('fs');
const util = require('util');
// Method 1: Using util.promisify
const readFilePromise = util.promisify(fs.readFile);
// Method 2: Manual conversion
function readFileAsync(path, encoding) {
return new Promise((resolve, reject) => {
fs.readFile(path, encoding, (error, data) => {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
}
// Usage
readFileAsync('file.txt', 'utf8')
.then(data => console.log(data))
.catch(error => console.error(error));
3. Async / Await
Async/await is syntactic sugar over promises, making asynchronous code look synchronous.
Async Functions:
-
asyncfunction always returns a promise -
awaitpauses execution until promise settles -
Can use
try/catchfor error handling
javascript
// Basic async function
async function fetchData() {
// await can only be used inside async functions
const data = await someAsyncOperation();
return data;
}
Example with Error Handling:
javascript
async function processFiles() {
try {
// Read first file
const data1 = await readFileAsync('file1.txt', 'utf8');
console.log('File 1:', data1);
// Read second file (waits for first to complete)
const data2 = await readFileAsync('file2.txt', 'utf8');
console.log('File 2:', data2);
// Process data
const result = processData(data1, data2);
return result;
} catch (error) {
// Handle any error in the try block
console.error('Error processing files:', error);
throw error; // Re-throw if needed
}
}
// Calling async function (returns promise)
processFiles()
.then(result => console.log('Success:', result))
.catch(error => console.error('Failed:', error));
Parallel Execution with Async/Await:
javascript
async function parallelExecution() {
try {
// Start multiple promises in parallel
const [user, posts, comments] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchComments(userId)
]);
console.log('All data fetched:', { user, posts, comments });
} catch (error) {
console.error('Error in parallel execution:', error);
}
}
// For-loop with async/await (sequential)
async function processItems(items) {
const results = [];
for (const item of items) {
const result = await processItem(item); // Process one at a time
results.push(result);
}
return results;
}
// For-loop with Promise.all (parallel)
async function processItemsParallel(items) {
const promises = items.map(item => processItem(item));
return await Promise.all(promises); // Process all at once
}
IIFE with Async/Await:
javascript
// Immediately Invoked Async Function Expression
(async () => {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error(error);
}
})();
4. Event Loop (Basic Understanding)
The Event Loop is what allows Node.js to perform non-blocking I/O operations despite being single-threaded.
Simplified Event Loop Architecture:
text
┌───────────────────────────┐
│ Call Stack │ ← Executes synchronous code
└─────────────┬─────────────┘
│
┌─────────────▼─────────────┐
│ Node APIs │ ← Handles async operations (fs, http, timers)
└─────────────┬─────────────┘
│
┌─────────────▼─────────────┐
│ Callback/Task Queue │ ← Stores completed async callbacks
│ (Microtask Queue) │ ← Stores promises (.then, .catch)
└─────────────┬─────────────┘
│
┌─────────────▼─────────────┐
│ Event Loop │ ← Moves callbacks to call stack when empty
└───────────────────────────┘
Event Loop Phases:
-
Timers: Execute
setTimeout()andsetInterval()callbacks -
Pending Callbacks: Execute I/O callbacks deferred to next loop
-
Idle, Prepare: Internal use
-
Poll: Retrieve new I/O events; execute I/O callbacks
-
Check: Execute
setImmediate()callbacks -
Close Callbacks: Execute close events (socket.on('close', ...))
Example Demonstrating Event Loop:
javascript
console.log('1: Start');
setTimeout(() => {
console.log('2: Timeout callback');
}, 0);
Promise.resolve()
.then(() => {
console.log('3: Promise resolved');
});
console.log('4: End');
// Output order:
// 1: Start
// 4: End
// 3: Promise resolved ← Microtask (Promise) runs before Macrotask (setTimeout)
// 2: Timeout callback
Microtasks vs Macrotasks:
-
Microtasks: Promises,
process.nextTick(),queueMicrotask() -
Macrotasks:
setTimeout,setInterval,setImmediate, I/O callbacks
Priority: Microtasks run after each macrotask, before next macrotask.
5. Blocking vs Non-blocking Code
Blocking (Synchronous) Code:
Code that stops execution until operation completes.
javascript
// Blocking example
const fs = require('fs');
console.log('Start reading file...');
// BLOCKING - stops execution until file is read
const data = fs.readFileSync('largefile.txt', 'utf8');
console.log('File content:', data.length, 'characters');
console.log('Program continues...'); // Won't run until file is read
Characteristics:
-
Simple to write and understand
-
Execution waits for operation to complete
-
Can cause performance issues
-
Not suitable for I/O operations in servers
Non-blocking (Asynchronous) Code:
Code that continues execution while operation runs in background.
javascript
// Non-blocking example
const fs = require('fs');
console.log('Start reading file...');
// NON-BLOCKING - continues execution immediately
fs.readFile('largefile.txt', 'utf8', (error, data) => {
// This callback runs AFTER file is read
console.log('File content:', data.length, 'characters');
});
console.log('Program continues immediately...'); // Runs immediately
// Output: Start reading file... → Program continues immediately... → File content...
// With async/await (non-blocking but looks synchronous)
async function readFileNonBlocking() {
console.log('Start reading file...');
// Doesn't block other operations
const data = await fs.promises.readFile('largefile.txt', 'utf8');
console.log('File content:', data.length, 'characters');
}
readFileNonBlocking();
console.log('This runs while file is being read');
Comparison Table:
| Aspect | Blocking Code | Non-blocking Code |
|---|---|---|
| Execution | Waits for operation | Continues immediately |
| Performance | Can be slow for I/O | Efficient for I/O |
| Concurrency | Poor (one at a time) | Excellent (many at once) |
| Complexity | Simple to write | More complex patterns |
| Use Cases | CLI tools, scripts | Web servers, APIs, real-time apps |
| Error Handling | Try/catch | Callbacks, promises, async/await |
Practical Example: Web Server
javascript
const http = require('http');
// BLOCKING server (BAD - blocks all requests)
const blockingServer = http.createServer((req, res) => {
// This blocks the entire thread!
const data = fs.readFileSync('largefile.txt', 'utf8');
res.end(data); // No other requests can be processed until this completes
});
// NON-BLOCKING server (GOOD - handles concurrent requests)
const nonBlockingServer = http.createServer(async (req, res) => {
// This doesn't block - other requests can be processed
const data = await fs.promises.readFile('largefile.txt', 'utf8');
res.end(data);
});
nonBlockingServer.listen(3000, () => {
console.log('Server running on port 3000');
});
Choosing Between Approaches:
Use Blocking When:
-
Scripts/CLI tools where order matters
-
Configuration loading at startup
-
Simple, sequential operations
-
When simplicity is more important than performance
Use Non-blocking When:
-
Web servers/APIs
-
Database operations
-
File I/O in servers
-
Multiple concurrent operations
-
Real-time applications
Performance Impact Example:
javascript
// Simulating blocking vs non-blocking
function blockingOperation() {
console.time('blocking');
// Simulate CPU-intensive work
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
}
console.timeEnd('blocking');
return sum;
}
function nonBlockingOperation() {
return new Promise((resolve) => {
console.time('non-blocking');
// Defer to next tick
setImmediate(() => {
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
}
console.timeEnd('non-blocking');
resolve(sum);
});
});
}
// Blocking example
console.log('Before blocking');
blockingOperation(); // Blocks everything
console.log('After blocking'); // Only appears after operation
// Non-blocking example
console.log('Before non-blocking');
nonBlockingOperation().then(() => {
console.log('Non-blocking completed');
});
console.log('After non-blocking init'); // Appears immediately
Understanding these asynchronous patterns is crucial for writing efficient, scalable Node.js applications!
Comments (0)
No comments yet. Be the first to comment!