Promises in JavaScript

What are Promises?

Promises are objects that represent the eventual completion or failure of an asynchronous operation and its resulting value. They provide a cleaner way to handle asynchronous code compared to callbacks.

Promise States

A Promise can be in one of three states:

  1. Pending: Initial state, neither fulfilled nor rejected
  2. Fulfilled: The operation completed successfully
  3. Rejected: The operation failed

Once a promise is fulfilled or rejected, it is settled and cannot change state again.

Creating Promises

// Basic Promise creation
const promise = new Promise((resolve, reject) => {
  // Asynchronous operation
  const success = true;
  
  if (success) {
    resolve('Operation succeeded');
  } else {
    reject(new Error('Operation failed'));
  }
});

Promise Methods

then(), catch(), finally()

promise
  .then(result => {
    console.log('Success:', result);
    return 'Next value';
  })
  .then(nextResult => {
    console.log('Chained result:', nextResult);
  })
  .catch(error => {
    console.error('Error:', error.message);
  })
  .finally(() => {
    console.log('Promise settled (fulfilled or rejected)');
  });

Promise.resolve() and Promise.reject()

// Create already resolved promise
const resolvedPromise = Promise.resolve('Already resolved');
resolvedPromise.then(value => console.log(value)); // 'Already resolved'

// Create already rejected promise
const rejectedPromise = Promise.reject(new Error('Already rejected'));
rejectedPromise.catch(error => console.error(error.message)); // 'Already rejected'

Promise Chaining

function getUserData(userId) {
  return fetch(`/api/users/${userId}`)
    .then(response => {
      if (!response.ok) {
        throw new Error('User not found');
      }
      return response.json();
    });
}

function getUserPosts(user) {
  return fetch(`/api/posts?userId=${user.id}`)
    .then(response => response.json());
}

// Chain promises
getUserData(1)
  .then(user => {
    console.log('User:', user);
    return getUserPosts(user);
  })
  .then(posts => {
    console.log('Posts:', posts);
  })
  .catch(error => {
    console.error('Error:', error.message);
  });

Error Handling in Promises

// Error propagation in promise chains
fetchData()
  .then(data => {
    // This error will be caught by the catch block
    if (!data.items) {
      throw new Error('Invalid data format');
    }
    return processData(data);
  })
  .then(result => {
    console.log('Result:', result);
  })
  .catch(error => {
    // Catches any error thrown in the chain
    console.error('Error:', error.message);
  });

// Handling specific errors
fetchData()
  .then(data => processData(data))
  .then(result => saveResult(result))
  .catch(error => {
    if (error.name === 'NetworkError') {
      // Handle network errors
      return fallbackData();
    }
    // Re-throw other errors
    throw error;
  })
  .then(finalResult => {
    console.log('Final result:', finalResult);
  })
  .catch(error => {
    console.error('Unhandled error:', error);
  });

Combining Multiple Promises

Promise.all()

Waits for all promises to resolve or for any to reject.

const promises = [
  fetch('/api/users').then(res => res.json()),
  fetch('/api/posts').then(res => res.json()),
  fetch('/api/comments').then(res => res.json())
];

Promise.all(promises)
  .then(([users, posts, comments]) => {
    console.log('Users:', users);
    console.log('Posts:', posts);
    console.log('Comments:', comments);
  })
  .catch(error => {
    // If any promise rejects, this catch will execute
    console.error('One of the requests failed:', error);
  });

Promise.allSettled()

Waits for all promises to settle (resolve or reject).

Promise.allSettled(promises)
  .then(results => {
    results.forEach((result, index) => {
      if (result.status === 'fulfilled') {
        console.log(`Promise ${index} fulfilled with:`, result.value);
      } else {
        console.log(`Promise ${index} rejected with:`, result.reason);
      }
    });
  });

Promise.race()

Returns the first promise to settle (resolve or reject).

const promise1 = new Promise(resolve => setTimeout(() => resolve('First'), 500));
const promise2 = new Promise(resolve => setTimeout(() => resolve('Second'), 100));

Promise.race([promise1, promise2])
  .then(result => console.log('Fastest promise:', result)) // 'Second'
  .catch(error => console.error('Error:', error));

Promise.any()

Returns the first promise to fulfill (or rejects if all reject).

const promise1 = Promise.reject(new Error('Error 1'));
const promise2 = new Promise(resolve => setTimeout(() => resolve('Success'), 200));
const promise3 = Promise.reject(new Error('Error 3'));

Promise.any([promise1, promise2, promise3])
  .then(result => console.log('First fulfilled promise:', result)) // 'Success'
  .catch(error => console.error('All promises rejected:', error));

Async/Await with Promises

async function fetchUserData(userId) {
  try {
    // Await pauses execution until the promise resolves
    const response = await fetch(`/api/users/${userId}`);
    
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    
    const userData = await response.json();
    
    // Get user posts
    const postsResponse = await fetch(`/api/posts?userId=${userId}`);
    const posts = await postsResponse.json();
    
    return {
      user: userData,
      posts: posts
    };
  } catch (error) {
    console.error('Error fetching user data:', error);
    throw error; // Re-throw to allow caller to handle
  }
}

// Usage
fetchUserData(1)
  .then(data => console.log('User data:', data))
  .catch(error => console.error('Failed to fetch user data:', error));

Common Promise Patterns

Promisification

Converting callback-based functions to promise-based:

// Callback-based function
function readFileCallback(path, callback) {
  fs.readFile(path, 'utf8', (error, data) => {
    if (error) {
      callback(error);
      return;
    }
    callback(null, data);
  });
}

// Promisified version
function readFilePromise(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, 'utf8', (error, data) => {
      if (error) {
        reject(error);
        return;
      }
      resolve(data);
    });
  });
}

// Usage
readFilePromise('config.json')
  .then(data => console.log('File content:', data))
  .catch(error => console.error('Error reading file:', error));

Delay/Timeout

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// Usage
async function processWithDelay() {
  console.log('Starting');
  await delay(2000); // Wait for 2 seconds
  console.log('After 2 seconds');
}

// Adding timeout to a promise
function timeoutPromise(promise, ms) {
  const timeout = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Operation timed out')), ms);
  });
  
  return Promise.race([promise, timeout]);
}

// Usage
timeoutPromise(fetch('https://api.example.com/data'), 5000)
  .then(response => response.json())
  .then(data => console.log('Data:', data))
  .catch(error => console.error('Error or timeout:', error.message));

Retry Logic

async function fetchWithRetry(url, options = {}, retries = 3, backoff = 300) {
  try {
    return await fetch(url, options);
  } catch (error) {
    if (retries <= 0) {
      throw error;
    }
    
    // Wait before retrying (exponential backoff)
    await new Promise(resolve => setTimeout(resolve, backoff));
    
    return fetchWithRetry(url, options, retries - 1, backoff * 2);
  }
}

// Usage
fetchWithRetry('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log('Data:', data))
  .catch(error => console.error('All retries failed:', error));

Sequential vs. Parallel Execution

// Sequential execution
async function processSequentially(items) {
  const results = [];
  
  for (const item of items) {
    // Wait for each promise to resolve before continuing
    const result = await processItem(item);
    results.push(result);
  }
  
  return results;
}

// Parallel execution with concurrency limit
async function processWithConcurrencyLimit(items, concurrency = 3) {
  const results = [];
  const running = [];
  
  for (const item of items) {
    // Create a promise that removes itself from running when done
    const promise = processItem(item)
      .then(result => {
        results.push(result);
        running.splice(running.indexOf(promise), 1);
        return result;
      });
    
    running.push(promise);
    
    if (running.length >= concurrency) {
      // Wait for one task to complete before adding more
      await Promise.race(running);
    }
  }
  
  // Wait for all remaining tasks
  await Promise.all(running);
  
  return results;
}

Promise Performance Considerations

// Avoid creating unnecessary promises
function unnecessaryPromise() {
  // Inefficient - creates a new promise for a value we already have
  return Promise.resolve(42);
}

function betterApproach() {
  // More efficient - just return the value
  return 42;
}

// Avoid nesting promises
function nestedPromises() {
  return getData().then(data => {
    return getMoreData(data).then(moreData => {
      return evenMoreData(moreData);
    });
  });
}

function flattenedPromises() {
  return getData()
    .then(data => getMoreData(data))
    .then(moreData => evenMoreData(moreData));
}

Common Promise Mistakes

1. Forgetting to return promises in then()

// Incorrect - the second then doesn't wait for processData
fetchData()
  .then(data => {
    processData(data); // Returns a promise but we don't return it
  })
  .then(result => {
    // result is undefined, not the result of processData
    console.log(result);
  });

// Correct
fetchData()
  .then(data => {
    return processData(data); // Return the promise
  })
  .then(result => {
    console.log(result); // Now result contains processData's resolved value
  });

2. Not handling rejections

// Missing error handling
fetchData()
  .then(data => processData(data))
  .then(result => console.log(result));
  // No .catch() - unhandled promise rejection!

// Proper error handling
fetchData()
  .then(data => processData(data))
  .then(result => console.log(result))
  .catch(error => console.error('Error:', error));

3. Promise executor errors

// Errors in the executor function are automatically caught
const promise = new Promise((resolve, reject) => {
  throw new Error('Error in executor'); // This becomes a rejected promise
});

promise.catch(error => console.error(error.message)); // 'Error in executor'

Interview Tips

  • Explain the three states of a Promise and how transitions occur
  • Describe the difference between Promise.all, Promise.race, Promise.allSettled, and Promise.any
  • Explain how error handling works in promise chains
  • Discuss the advantages of Promises over callbacks
  • Demonstrate how to convert callback-based code to Promises
  • Explain common Promise patterns and anti-patterns
  • Describe how async/await relates to Promises

Test Your Knowledge

Take a quick quiz to test your understanding of this topic.