BRUCE_FEBRUCE_FE

EN/CH Mode

BRUCE_FE JS Interview Notes - Promise & Async/Await In-depth Analysis

In-depth explanation of Promise and Async/Await mechanisms, usage, common methods and implementation in JavaScript to help you easily tackle related interview questions.

影片縮圖

Lazy to read articles? Then watch videos!

Promise Basic Concepts

Promise is the standard way to handle asynchronous operations in JavaScript, representing an operation that hasn't completed yet but is expected to in the future. A Promise has three states:

  • Pending: Initial state, neither fulfilled nor rejected
  • Fulfilled: Operation completed successfully
  • Rejected: Operation failed
const promise = new Promise((resolve, reject) => {
  // Asynchronous operation
  if (/* success */) {
    resolve(value);  // state changes to fulfilled
  } else {
    reject(error);   // state changes to rejected
  }
});

Why Do We Need Promises?

Promises solve several issues with traditional callbacks, providing a more elegant way to handle asynchronous operations:

  • Avoiding callback hell: Solves code maintainability issues caused by nested callbacks
  • Unified error handling: Using the catch method to handle errors uniformly
  • Chaining: Implementing clear flow control through the then method
  • Better semantics: Promise state transitions are more intuitive for programming
// Traditional callback approach
doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log(finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

// Promise approach
doSomething()
  .then(result => doSomethingElse(result))
  .then(newResult => doThirdThing(newResult))
  .then(finalResult => console.log(finalResult))
  .catch(failureCallback);

Advantages of Async/Await

Async/Await is syntactic sugar built on top of Promises, providing a more intuitive way to handle asynchronous operations:

  • Synchronous style: Makes asynchronous code look like synchronous code
  • Error handling: Can use traditional try/catch for error handling
  • Easier debugging: Can debug line by line like synchronous code
  • Conditional handling: Easier to implement complex conditional logic
async function fetchUserData() {
  try {
    const response = await fetch('/api/user');
    const userData = await response.json();
    return userData;
  } catch (error) {
    console.error('Error fetching user data:', error);
    throw error;
  }
}

Promise Static Methods

Promise.all()

Executes multiple Promises in parallel, waiting for all to complete. If any Promise fails, the entire operation fails.

Promise.all([
  fetch('/api/users'),
  fetch('/api/posts')
]).then(([users, posts]) => {
  // process results
});

Promise.race()

Returns the result of the first completed Promise, regardless of success or failure.

Promise.race([
  fetch('/api/data'),
  new Promise((_, reject) =>
    setTimeout(() => reject('Timeout'), 5000)
  )
]);

Promise.any()

Returns the result of the first successful Promise. Only rejects when all Promises fail.

Promise.any([
  fetch('https://api1.example.com'),
  fetch('https://api2.example.com')
]).then(firstSuccess => {
  // Handle the first successful result
});

Promise.allSettled()

Waits for all Promises to complete, returning the status of each Promise result.

Promise.allSettled([
  fetch('/api/users'),
  fetch('/api/posts')
]).then(results => {
  // process all results
});

🔥 Common Interview Questions

(1) What are the state transition rules for Promises?

Answer: Promise state transitions have the following characteristics:

  • Initial state is Pending
  • Can only transition from Pending to Fulfilled or Rejected
  • State transitions are irreversible; once changed, they cannot be changed again
  • When state transitions occur, corresponding callbacks (then or catch) are triggered

(2) What's the difference between Promise.all and Promise.race?

Answer:

FeaturePromise.allPromise.race
Completion conditionAll Promises completeAny Promise completes
Failure handlingFails if any Promise failsUses the first completed result
Return valueArray of all resultsResult of first completion

(3) What's the relationship between async/await and Promises?

Answer:

  • async/await is syntactic sugar built on top of Promises
  • async functions always return a Promise
  • await can only be used inside async functions
  • await can wait for any 'thenable' object (objects that implement the then method)
// async functions always return a Promise
async function example() {
  return "Hello";
}

// Equivalent to
function example() {
  return Promise.resolve("Hello");
}

// Verify that async returns a Promise
console.log(example()); // Promise {<fulfilled>: "Hello"}

// await unwraps the value resolved by the Promise
async function example() {
  const result = await example(); // Waits for Promise.resolve("Hello") result
  console.log(result); // "Hello"
  
  // Equivalent to
  example().then(value => console.log(value)); // "Hello"
}

// Complete comparison example
// Promise approach
function fetchData() {
  return fetch('/api/data')
    .then(response => response.json())
    .then(data => data)
    .catch(error => console.error(error));
}

// async/await approach - more readable synchronous style
async function fetchData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return data; // automatically wrapped as Promise.resolve(data)
  } catch (error) {
    console.error(error);
  }
}

(4) How to implement your own Promise.all?

Answer: We can implement a simple MyPromise.all function that mimics the functionality of Promise.all:

function myPromiseAll(promises) {
  return new Promise((resolve, reject) => {
    // If input is not an array, reject immediately
    if (!Array.isArray(promises)) {
      return reject(new TypeError('promises must be an array'));
    }
    
    const results = [];  // Store all promise results
    let completed = 0;   // Number of completed promises
    
    // If empty array, return empty array result
    if (promises.length === 0) {
      return resolve(results);
    }
    
    // Loop through all promises
    promises.forEach((promise, index) => {
      // Use Promise.resolve to handle non-Promise values
      Promise.resolve(promise)
        .then(result => {
          results[index] = result;  // Maintain original order
          completed++;
          
          // When all promises complete, resolve the final result
          if (completed === promises.length) {
            resolve(results);
          }
        })
        .catch(error => {
          // If any promise rejects, the entire myPromiseAll rejects
          reject(error);
        });
    });
  });
}

// Usage example
const p1 = Promise.resolve(1);
const p2 = new Promise(resolve => setTimeout(() => resolve(2), 100));
const p3 = Promise.resolve(3);

myPromiseAll([p1, p2, p3])
  .then(results => console.log(results))  // [1, 2, 3]
  .catch(error => console.error(error));

Key points of this implementation:

  • Returns a new Promise
  • Tracks completion status of all Promises
  • Maintains the original order of results
  • Immediately rejects the entire Promise when any Promise fails
  • Uses Promise.resolve to handle non-Promise values