Async Patterns

15 questions found

What is a callback function in Node.js, and what is 'callback hell'?

Beginner
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.
// 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);
    });
  });
});
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.

Common follow-ups: What does 'error-first' mean in the error-first callback convention, and why was it adopted?;How would util.promisify() convert an existing callback-based function to return a promise instead?

Error Handling;Async Patterns

What is a Promise in JavaScript, and what are its three possible states?

Beginner
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.

Common follow-ups: What happens if resolve() is called after reject() has already been called on the same promise?;How does Promise.prototype.finally() differ from adding logic inside both .then() and .catch()?

Async Patterns;Error Handling

What is the difference between Promise.all(), Promise.allSettled(), Promise.race(), and Promise.any()?

Intermediate
Promise.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.

Common follow-ups: How would you implement a request timeout using Promise.race()?;What's a scenario where Promise.any() is specifically the right choice over Promise.race()?

Async Patterns;Error Handling

How does async/await relate to Promises under the hood, and what does 'await' actually do to program execution?

Intermediate
async/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.

Common follow-ups: What happens if you forget the 'await' keyword before a promise-returning call inside an async function?;Why does an unhandled rejection inside an async function not crash the process the same way a synchronous throw would, without additional handling?

Error Handling;Event Loop & Non-blocking IO

What is the difference between running async operations sequentially with multiple awaits versus concurrently with Promise.all()?

Intermediate
Writing '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.

Common follow-ups: In what specific scenario is sequential awaiting actually the correct choice rather than a mistake?;How does this concurrency differ from true parallelism, given Node.js still runs on a single main thread?

Async Patterns;Performance Optimization & Profiling

What is an async generator, and how does 'for await...of' let you consume asynchronously produced data?

Advanced
An 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.

Common follow-ups: How do async generators relate to Node.js readable streams, given both represent asynchronous sequences of data?;What happens if the consuming 'for await...of' loop is exited early with a break -- does the generator clean up properly?

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?

Advanced
An 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.

Common follow-ups: Why is it considered bad practice to use these handlers to simply swallow the error and let the process keep running?;How do you prevent a specific promise from ever becoming an unhandled rejection in the first place?

Error Handling;Debugging & Diagnostics

How do you implement a retry-with-exponential-backoff pattern for a flaky async operation in Node.js?

Intermediate
Exponential 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.

Common follow-ups: Which types of errors should actually be retried versus failed immediately (like a 400 Bad Request versus a 503 Service Unavailable)?;How does adding jitter specifically help prevent a 'thundering herd' problem?

Error Handling;Cloud & DevOps

What is util.promisify(), and when would you still need it in modern Node.js?

Intermediate
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.

Common follow-ups: What convention must a callback-based function follow for util.promisify() to work correctly on it?;How do you handle a callback-based function that doesn't follow the standard error-first convention?

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?

Advanced
forEach() 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.

Common follow-ups: Why does map() work correctly with async callbacks when combined with Promise.all(), while forEach() doesn't work in the same pattern?;What's the performance tradeoff between the for...of sequential approach and the Promise.all with map concurrent approach?

Error Handling;Arrays & Array Methods

Showing 1–10 of 15