🟨 JavaScript Q73 / 121

Explain advanced async patterns.

AI-Powered Answer ✓ Answered

Asynchronous operations are fundamental to modern JavaScript applications, allowing non-blocking execution crucial for user experience and server responsiveness. While `callbacks`, `Promises`, and `async/await` handle basic async flows, several advanced patterns extend capabilities for complex scenarios like concurrent operations, error recovery, stream processing, and background tasks.

1. Promise Combinators: Promise.all(), Promise.allSettled(), Promise.race(), Promise.any()

Promise combinators are static methods on the Promise object that take an iterable of promises and return a single promise that resolves based on the fulfillment or rejection of the input promises.

Promise.all(iterable)

Executes multiple promises concurrently and waits for all of them to fulfill. If any of the promises reject, the returned promise immediately rejects with the reason of the first promise that rejected. It's ideal when all operations are critical and interdependent.

javascript
async function fetchData(url) {
  const response = await fetch(url);
  if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
  return response.json();
}

async function loadAllUserData() {
  try {
    const [users, posts, comments] = await Promise.all([
      fetchData('https://api.example.com/users'),
      fetchData('https://api.example.com/posts'),
      fetchData('https://api.example.com/comments')
    ]);
    console.log('All data loaded:', { users, posts, comments });
  } catch (error) {
    console.error('Failed to load all data:', error.message);
  }
}

Promise.allSettled(iterable)

Similar to Promise.all(), but it waits for all promises to settle (either fulfill or reject), and always resolves. The returned promise resolves with an array of objects, each describing the outcome of a promise (status: 'fulfilled' | 'rejected', value | reason). Useful when you want to execute independent operations and gather all results, even if some fail.

javascript
const promises = [
  Promise.resolve(3),
  new Promise(resolve => setTimeout(() => resolve(42), 100)),
  Promise.reject('Error occurred!')
];

Promise.allSettled(promises).then((results) => {
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`Promise ${index} fulfilled with value: ${result.value}`);
    } else {
      console.error(`Promise ${index} rejected with reason: ${result.reason}`);
    }
  });
  // Output:
  // Promise 0 fulfilled with value: 3
  // Promise 1 fulfilled with value: 42
  // Promise 2 rejected with reason: Error occurred!
});

Promise.race(iterable)

Returns a promise that fulfills or rejects as soon as one of the promises in the iterable fulfills or rejects, with the value or reason from that promise. Useful for time-outs or competitive scenarios where you only care about the first outcome.

javascript
function timeout(ms) {
  return new Promise((resolve, reject) => setTimeout(() => reject(new Error('Timeout!')), ms));
}

async function fetchWithTimeout(url, ms) {
  try {
    const response = await Promise.race([
      fetch(url),
      timeout(ms)
    ]);
    return response.json();
  } catch (error) {
    console.error('Fetch or timeout error:', error.message);
    throw error;
  }
}

fetchWithTimeout('https://api.example.com/slow-data', 2000);
// If fetch resolves faster than 2s, we get data. Otherwise, a timeout error.

Promise.any(iterable) (ES2021)

Returns a promise that fulfills as soon as any of the promises in the iterable fulfills, with the value of that promise. If all of the promises reject, then the returned promise rejects with an AggregateError containing an array of all rejection reasons. Useful when you need any one successful outcome.

javascript
const fasterPromise = new Promise(resolve => setTimeout(() => resolve('First success!'), 100));
const slowerPromise = new Promise(resolve => setTimeout(() => resolve('Second success!'), 500));
const failingPromise = Promise.reject('Failed!');

Promise.any([failingPromise, slowerPromise, fasterPromise])
  .then(value => console.log(value)) // Logs: First success!
  .catch(error => console.error(error));

2. Async Generators and Iterators

Async generators (functions declared with async function*) combine asynchronous behavior with the yield keyword. They return an async iterator, allowing you to await inside the generator and for await...of to consume the yielded values. This is powerful for handling streams of asynchronously produced data, like fetching paginated API results or reading from a file stream.

javascript
async function* fetchPokemonPages(limit) {
  let offset = 0;
  let nextUrl = `https://pokeapi.co/api/v2/pokemon?limit=${limit}&offset=${offset}`;

  while (nextUrl) {
    const response = await fetch(nextUrl);
    if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
    const data = await response.json();
    yield data.results; // Yields an array of Pokémon names
    nextUrl = data.next;
  }
}

async function processAllPokemon() {
  for await (const pokemonBatch of fetchPokemonPages(10)) {
    console.log(`Processing batch of ${pokemonBatch.length} Pokémon:`);
    pokemonBatch.forEach(pokemon => console.log(`- ${pokemon.name}`));
    // Simulate some async processing per batch
    await new Promise(resolve => setTimeout(resolve, 500));
  }
  console.log('Finished processing all Pokémon.');
}

processAllPokemon();

3. Event Emitters (Node.js)

The EventEmitter class (primarily in Node.js, though similar patterns exist in browsers) allows for an observer pattern where objects can emit named events that cause registered listener functions to be called. It's a powerful way to decouple components, manage application flow, and handle custom events in an asynchronous, non-blocking manner.

javascript
const EventEmitter = require('events');

class MyWorker extends EventEmitter {
  constructor() {
    super();
    this.data = [];
  }

  async startWork() {
    this.emit('start', 'Worker started.');
    for (let i = 0; i < 3; i++) {
      // Simulate async work
      await new Promise(resolve => setTimeout(resolve, 1000));
      const item = `Item ${i + 1}`;
      this.data.push(item);
      this.emit('progress', item);
    }
    this.emit('complete', 'Worker finished successfully.');
  }
}

const worker = new MyWorker();

worker.on('start', (message) => {
  console.log(message);
});

worker.on('progress', (item) => {
  console.log(`Processed: ${item}, current data:`, worker.data);
});

worker.on('complete', (message) => {
  console.log(message);
  console.log('Final data:', worker.data);
});

worker.startWork().catch(err => console.error('Worker failed:', err));

4. Web Workers (Browser)

Web Workers enable JavaScript code to run in a separate background thread, distinct from the main execution thread of a web page. This prevents CPU-intensive tasks from blocking the UI, keeping the application responsive. Communication between the main thread and a worker happens via messages (postMessage, onmessage).

Main Thread (index.html or app.js)

javascript
// app.js
if (window.Worker) {
  const myWorker = new Worker('worker.js');

  myWorker.postMessage({ command: 'calculateSum', num: 1000000000 });

  myWorker.onmessage = function(e) {
    console.log('Message from worker:', e.data);
    // Expected output: Message from worker: { result: 500000000500000000 }
  };

  myWorker.onerror = function(error) {
    console.error('Worker error:', error);
  };

  console.log('Main thread continues to run unblocked.');
}

Worker Thread (worker.js)

javascript
// worker.js
self.onmessage = function(e) {
  if (e.data.command === 'calculateSum') {
    let sum = 0;
    for (let i = 1; i <= e.data.num; i++) {
      sum += i;
    }
    self.postMessage({ result: sum });
  }
};

5. AbortController for Cancellable Promises

The AbortController interface (available in browsers and Node.js for fetch, timers, etc.) allows you to cancel one or more DOM requests or other asynchronous operations. It provides an AbortSignal object that can be passed to functions that support aborting, enabling you to explicitly stop an in-flight operation. This is crucial for resource management, preventing race conditions, and improving user experience (e.g., canceling a slow fetch if the user navigates away).

javascript
async function fetchDataWithCancellation(url) {
  const controller = new AbortController();
  const signal = controller.signal;

  const timeoutId = setTimeout(() => controller.abort(), 5000); // Auto-abort after 5 seconds

  try {
    console.log(`Fetching ${url}...`);
    const response = await fetch(url, { signal });
    clearTimeout(timeoutId);

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    console.log('Data fetched successfully:', data);
    return data;
  } catch (error) {
    clearTimeout(timeoutId);
    if (error.name === 'AbortError') {
      console.warn('Fetch request was aborted:', error.message);
    } else {
      console.error('Fetch error:', error.message);
    }
    throw error;
  }
}

// Example usage:
const requestPromise = fetchDataWithCancellation('https://api.example.com/some-data');

// You can also manually abort later:
// setTimeout(() => { 
//   controller.abort(); 
//   console.log('Manually aborted fetch.');
// }, 2000);

Conclusion

These advanced asynchronous patterns offer robust solutions for managing complex concurrent tasks, handling errors gracefully, and optimizing application performance and responsiveness. Mastering them is key to building sophisticated and efficient JavaScript applications that provide a seamless user experience, whether in the browser or on the server.