// Callback hell: deeply nested, hard to follow
fs.readFile('a.txt', (err, a) => {
fs.readFile('b.txt', (err, b) => {
fs.readFile('c.txt', (err, c) => {
console.log(a, b, c);
});
});
});
Topics
40
Advanced Node.js
Architecture & Design Patterns
Async Patterns
Authentication & Authorization
Authentication & Authorization (JWT, OAuth, Passport)
Background Jobs & Queues
Caching
Caching with Redis
Child Processes & Process Management
CLI Tools & Scripting with Node.js
Cloud & DevOps
Clustering & Worker Threads
Core Node.js Modules
Databases
Databases & ORMs (MongoDB/Mongoose, SQL/Sequelize)
Debugging & Diagnostics
Deployment & Process Managers (PM2)
Docker & Containerization for Node.js
Docker & Deployment
Email & Notifications
Environment Variables & Configuration
Error Handling
Event Loop & Non-blocking IO
Events & EventEmitter
Express & Middleware
File System & File Processing
File System (fs) Module
File Uploads & Media Processing
Git & Project Management
Global Objects & the process Object
GraphQL
GraphQL with Node.js
HTTP & HTTPS Modules
HTTP & Web Servers
Logging & Monitoring
Message Queues (RabbitMQ & Kafka)
Microservices Architecture with Node.js
Node.js Fundamentals & Runtime Architecture
Path & OS Modules
Performance Optimization & Profiling
Async Patterns
15 questions found
A callback is a function passed as an argument to another function, invoked once that function's asynchronous work completes -- Node's original core APIs (fs, http, etc.) are all built around this 'error-first callback' convention. Callback hell refers to the deeply nested, hard-to-read pyramid of code that results from chaining multiple dependent asynchronous callback-based operations, one inside another, which promises and async/await were both introduced specifically to solve.
Real-world example
A legacy codebase with five levels of nested callbacks handling a user-registration flow (validate, hash password, save to database, send email, log event) is refactored to use async/await, immediately making the control flow readable top-to-bottom and making error handling consistent via a single try/catch.
Error Handling;Async Patterns
A Promise is an object representing the eventual result of an asynchronous operation, existing in exactly one of three states at any time: pending (the operation hasn't completed yet), fulfilled (the operation succeeded, with a resulting value), or rejected (the operation failed, with a reason/error) -- once a promise transitions to fulfilled or rejected it is permanently 'settled' and cannot change state again, which is what makes chaining .then()/.catch() predictable.
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = Math.random() > 0.5;
success ? resolve('Data loaded') : reject(new Error('Load failed'));
}, 1000);
});
promise.then(result => console.log(result)).catch(err => console.error(err.message));
Real-world example
A data-fetching function returns a Promise that resolves with the parsed API response or rejects with a descriptive error, letting every caller consistently use .then()/.catch() or await/try-catch rather than needing to know the internal implementation details of how the request was made.
Async Patterns;Error Handling
What is the difference between Promise.all(), Promise.allSettled(), Promise.race(), and Promise.any()?
IntermediatePromise.all() resolves when every promise in the array resolves, but rejects immediately as soon as any single promise rejects (short-circuiting the rest). Promise.allSettled() always waits for every promise to settle regardless of outcome, returning an array describing each result's status ('fulfilled' or 'rejected') rather than throwing -- useful when you need every result even if some fail. Promise.race() settles as soon as the first promise settles, whether fulfilled or rejected. Promise.any() resolves as soon as the first promise fulfills, only rejecting if every promise rejects.
const results = await Promise.allSettled([
fetch('/api/a'), fetch('/api/b'), fetch('/api/c')
]);
results.forEach(r => r.status === 'fulfilled' ? console.log(r.value) : console.error(r.reason));
// Promise.race for a timeout pattern
const result = await Promise.race([fetchData(), timeout(5000)]);
Real-world example
A dashboard fetching data from five independent microservices uses Promise.allSettled() rather than Promise.all(), so that if one service is temporarily down, the dashboard can still render data from the four services that succeeded instead of failing the entire page load.
Async Patterns;Error Handling
How does async/await relate to Promises under the hood, and what does 'await' actually do to program execution?
Intermediateasync/await is syntactic sugar built directly on top of Promises -- an async function always implicitly returns a Promise, and 'await' pauses execution of that specific async function (without blocking the rest of the event loop) until the awaited Promise settles, then either returns its resolved value or throws its rejection reason as a regular JavaScript exception, which is why try/catch works naturally with awaited code in a way it never could with raw .then() chains.
async function getUserData(id) {
try {
const user = await fetchUser(id); // pauses this function only, not the whole program
const posts = await fetchPosts(user.id);
return { user, posts };
} catch (err) {
console.error('Failed to load user data:', err.message);
throw err;
}
}
Real-world example
A team migrating a callback-heavy codebase to async/await finds their error handling becomes dramatically simpler, since a single try/catch block now correctly captures errors from multiple sequential awaited calls, replacing what had been several separate error-first callback checks scattered through nested functions.
Error Handling;Event Loop & Non-blocking IO
What is the difference between running async operations sequentially with multiple awaits versus concurrently with Promise.all()?
IntermediateWriting 'const a = await taskA(); const b = await taskB();' runs the two tasks sequentially -- taskB doesn't even start until taskA fully completes, wasting time when the tasks are independent. Starting both promises first (without awaiting immediately) and then awaiting them together via Promise.all() lets both operations run concurrently, since they're both already in flight by the time either await actually pauses execution, meaningfully reducing total wait time for independent I/O-bound operations.
// Sequential: total time = taskA time + taskB time
const a = await taskA();
const b = await taskB();
// Concurrent: total time = max(taskA time, taskB time)
const [a2, b2] = await Promise.all([taskA(), taskB()]);
Real-world example
A page-load handler fetching a user's profile, recent orders, and notification count from three independent database queries switches from three sequential awaits to a single Promise.all(), cutting the endpoint's response time roughly to the duration of the single slowest query instead of the sum of all three.
Async Patterns;Performance Optimization & Profiling
What is an async generator, and how does 'for await...of' let you consume asynchronously produced data?
AdvancedAn async generator (declared with 'async function*') can yield values over time, each potentially resolved from an awaited asynchronous operation, producing an async iterator that a consumer walks through using 'for await...of' -- this is especially useful for processing paginated API results, streaming database cursors, or any data source that arrives incrementally rather than all at once, without needing to load the entire dataset into memory first.
async function* fetchAllPages(url) {
let nextUrl = url;
while (nextUrl) {
const response = await fetch(nextUrl);
const page = await response.json();
yield page.items;
nextUrl = page.nextPageUrl;
}
}
for await (const items of fetchAllPages('/api/users')) {
console.log(`Got a page of ${items.length} users`);
}
Real-world example
A data-export job consuming a paginated third-party API uses an async generator to yield each page of results as it arrives, processing and writing each batch to disk immediately rather than accumulating potentially hundreds of thousands of records in memory before starting to write anything.
Iterators & Generators;Streams & Buffers
What is the difference between an unhandled promise rejection and an uncaught exception in Node.js, and how does Node handle each by default?
AdvancedAn uncaught exception is a synchronous throw that propagates all the way up without being caught, which crashes the Node.js process immediately by default (via the 'uncaughtException' event, which should only ever be used for cleanup/logging before exiting, never to keep the process alive). An unhandled promise rejection happens when a rejected promise has no .catch() handler attached anywhere in its chain -- historically this only printed a warning, but as of modern Node.js versions the default behavior is to also terminate the process (configurable via the --unhandled-rejections flag), treating it with the same seriousness as an uncaught exception.
process.on('uncaughtException', (err) => {
console.error('Uncaught exception, shutting down:', err);
process.exit(1); // cleanup only, never suppress and continue
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection:', reason);
process.exit(1);
});
Real-world example
A production incident where a service silently stopped processing jobs (rather than crashing loudly) is traced to an unhandled promise rejection inside a fire-and-forget async call; adding a process-level unhandledRejection handler that logs and exits ensures future occurrences fail loudly and get restarted by the process manager instead of hanging silently.
Error Handling;Debugging & Diagnostics
How do you implement a retry-with-exponential-backoff pattern for a flaky async operation in Node.js?
IntermediateExponential backoff retries a failed operation with progressively longer delays between attempts (typically doubling each time, often with added random 'jitter' to avoid multiple clients retrying in lockstep) rather than retrying immediately or at a fixed interval, reducing load on an already-struggling downstream service while still recovering automatically once it becomes available again.
async function retryWithBackoff(fn, maxRetries = 5, baseDelay = 200) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === maxRetries - 1) throw err;
const delay = baseDelay * 2 ** attempt + Math.random() * 100;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Real-world example
A payment-service client wraps its API calls in a retryWithBackoff helper so that transient network blips or brief rate-limiting from the payment provider are automatically retried up to five times with increasing delays, rather than immediately failing the customer's checkout on the very first transient error.
Error Handling;Cloud & DevOps
util.promisify() converts a traditional Node.js error-first callback-style function into one that returns a Promise instead, letting it be used with async/await -- this remains useful when working with older core APIs or third-party libraries that haven't yet adopted the newer promise-based APIs that Node.js has increasingly added natively (like fs.promises or node:timers/promises).
const util = require('node:util');
const fs = require('node:fs');
const readFileAsync = util.promisify(fs.readFile);
const data = await readFileAsync('config.json', 'utf-8');
// Modern alternative: many core modules now ship promise versions directly
const fsPromises = require('node:fs/promises');
const data2 = await fsPromises.readFile('config.json', 'utf-8');
Real-world example
A codebase depending on an older third-party callback-based Redis client wraps its get/set methods with util.promisify() to use them consistently with the rest of an async/await-based application, without needing to switch Redis client libraries just to gain promise support.
Async Patterns;Core Node.js Modules
What is a common pitfall when using async/await inside Array.prototype.forEach(), and how do you fix it?
AdvancedforEach() does not wait for or respect returned promises at all -- it ignores the return value of its callback entirely, so if the callback is an async function, forEach() fires them all off essentially in parallel and immediately returns, without ever waiting for any of them to complete, which is almost never the intended behavior and can silently swallow rejected promises entirely since there's nothing awaiting them.
// Broken: doesn't wait for any of the async operations
items.forEach(async (item) => { await processItem(item); });
console.log('Done!'); // logs immediately, before any items are actually processed
// Fixed: use a for...of loop for sequential processing
for (const item of items) { await processItem(item); }
console.log('Done!'); // correctly logs only after all items are processed
// Or Promise.all with map() for concurrent processing
await Promise.all(items.map(item => processItem(item)));
Real-world example
A batch job that silently only processed a fraction of its records was traced to using items.forEach(async item => ...) instead of a for...of loop, which caused the function to return and the process to exit before most of the asynchronous database writes had actually completed.
Error Handling;Arrays & Array Methods
Showing 1–10 of 15