JavaScriptConcepts

Promises in JavaScript

Introduction

Promises are a modern way to handle asynchronous operations in JavaScript. They represent a value that may be available now, later, or never. Promises help you write cleaner, more readable code compared to callbacks.


What is a Promise?

A Promise is a JavaScript object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. Think of it as a “placeholder” for a value that will be available in the future.

A Promise can be in one of three states:


Real World Example: Promises in Action

const githubUserInfoURL = "https://api.github.com/users/adityasrivastava29"; 

const user = fetch(githubUserInfoURL);
// Fetch returns a Promise
// user is a Promise object that will resolve to the response of the fetch operation
console.log(user); // This will log a Promise object
user.then((response) => {
    return response.json(); // Convert the response to JSON
}).then((data) => {
    console.log(data); // This will log the user data
}).catch((error) => {
    console.error("Error fetching user data:", error); // Handle any errors
}); // Catch any errors in the promise chain

Why Do We Need Promises?

Before Promises, JavaScript handled asynchronous operations with callbacks, which led to several problems:

  1. Callback Hell
// Without Promises - Callback Hell
getData(function(a) {
    getMoreData(a, function(b) {
        getEvenMoreData(b, function(c) {
            getFinalData(c, function(d) {
                // Finally do something with d
                console.log(d);
            });
        });
    });
});
  1. Error Handling Complexity
    // Callback error handling is messy
    getData(function(err, a) {
     if (err) {
         handleError(err);
         return;
     }
     getMoreData(a, function(err, b) {
         if (err) {
             handleError(err);
             return;
         }
         // More nested error checking...
     });
    });
    
  2. No Built-in Composition Callbacks don’t compose well - you can’t easily combine multiple asynchronous operations.

Promise Solutions

Promises solve these issues by providing:

//With Promises - Clean and readable
getData()
    .then(a => getMoreData(a))
    .then(b => getEvenMoreData(b))
    .then(c => getFinalData(c))
    .then(d => console.log(d))
    .catch(err => handleError(err));

Creating a Promise

The constructor syntax for a promise object is:

let promise = new Promise(function(resolve, reject) {
  // executor (the producing code, "singer")
});

The function passed to new Promise is called the executor. When new Promise is created, the executor runs automatically. It contains the producing code which should eventually produce the result. the executor is the “singer”.

Its arguments resolve and reject are callbacks provided by JavaScript itself. Our code is only inside the executor.

When the executor obtains the result, be it soon or late, doesn’t matter, it should call one of these callbacks:

So to summarize: the executor runs automatically and attempts to perform a job. When it is finished with the attempt, it calls resolve if it was successful or reject if there was an error.

The promise object returned by the new Promise constructor has these internal properties : state and result, We can’t directly access them. We can use the methods .then/.catch/.finally for that.


const myPromise = new Promise((resolve, reject) => {
  // Simulate async work
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('Operation successful!');
    } else {
      reject('Operation failed!');
    }
  }, 1000);
});

A promise that is either resolved or rejected is called “settled”, as opposed to an initially “pending” promise.

There can be only a single result or an error

The executor should call only one resolve or one reject. Any state change is final.

All further calls of resolve and reject are ignored:

let promise = new Promise(function(resolve, reject) {
  resolve("done");


  reject(new Error("")); // ignored
  setTimeout(() => resolve("")); // ignored
});

The idea is that a job done by the executor may have only one result or an error.

Also, resolve/reject expect only one argument (or none) and will ignore additional arguments.


Consuming a Promise

You use .then() to handle success and .catch() for errors.

myPromise
  .then(result => {
    console.log(result); // 'Operation successful!'
  })
  .catch(error => {
    console.error(error); // 'Operation failed!'
  });

Explanation:


Chaining Promises

You can chain multiple .then() calls for sequential async operations.

fetch(githubUserInfoURL)
  .then(response => response.json())
  .then(data => {
    return fetch(`https://api.github.com/users/${data.login}/repos`);
  })
  .then(response => response.json())
  .then(repos => {
    console.log(repos); // List of repositories
  })
  .catch(error => {
    console.error(error);
  });

Explanation:

Promise API

The Promise class provides 6 static methods for handling multiple promises efficiently.

1. Promise.all(promises)

Purpose: Execute promises in parallel, wait for ALL to complete
Behavior: Resolves when all promises resolve, rejects if ANY promise rejects
Result: Array of results in same order as input promises

NOTE : Please note that the order of the resulting array members is the same as in its source promises. Even though the first promise takes the longest time to resolve, it’s still first in the array of results.

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)),
  new Promise(resolve => setTimeout(() => resolve(2), 2000)),
  new Promise(resolve => setTimeout(() => resolve(3), 1000))
]).then(console.log); // [1, 2, 3] after 3 seconds

// Real-world example: Multiple API calls
let urls = ['api/user1', 'api/user2', 'api/user3'];
let requests = urls.map(url => fetch(url));
Promise.all(requests).then(responses => {
  // All requests completed successfully
});

Key Points:

2. Promise.allSettled(promises)

Purpose: Wait for ALL promises to complete, regardless of outcome
Behavior: Never rejects, always waits for all promises to settle
Result: Array of objects with {status, value/reason}

Promise.allSettled([
  fetch('https://api.github.com/users/validuser'),
  fetch('https://invalid-url'),
  Promise.resolve('direct value')
]).then(results => {
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`Success: ${result.value}`);
    } else {
      console.log(`Failed: ${result.reason}`);
    }
  });
});

Result Format:

Polyfill (for older browsers):

if (!Promise.allSettled) {
  Promise.allSettled = function(promises) {
    return Promise.all(promises.map(p => 
      Promise.resolve(p).then(
        value => ({status: 'fulfilled', value}),
        reason => ({status: 'rejected', reason})
      )
    ));
  };
}

3. Promise.race(promises)

Purpose: Get result of the FIRST promise to settle (resolve OR reject)
Behavior: Resolves/rejects with first settled promise
Result: Single value from the fastest promise

Promise.race([
  new Promise(resolve => setTimeout(() => resolve('slow'), 3000)),
  new Promise(resolve => setTimeout(() => resolve('fast'), 1000)),
  new Promise((_, reject) => setTimeout(() => reject('error'), 2000))
]).then(console.log); // 'fast' (after 1 second)

Use Cases:

4. Promise.any(promises)

Purpose: Get result of the FIRST promise to FULFILL (ignores rejections)
Behavior: Resolves with first successful promise, rejects only if ALL fail
Result: Single value from first successful promise, or AggregateError

Promise.any([
  new Promise((_, reject) => setTimeout(() => reject('Error 1'), 1000)),
  new Promise(resolve => setTimeout(() => resolve('Success!'), 2000)),
  new Promise(resolve => setTimeout(() => resolve('Also success'), 3000))
]).then(console.log); // 'Success!' (after 2 seconds)

// All fail scenario
Promise.any([
  Promise.reject('Error 1'),
  Promise.reject('Error 2')
]).catch(error => {
  console.log(error.constructor.name); // AggregateError
  console.log(error.errors); // ['Error 1', 'Error 2']
});

5. Promise.resolve(value)

Purpose: Create an immediately resolved promise
Use Case: Ensure consistent promise interface

// These are equivalent
Promise.resolve(42);
new Promise(resolve => resolve(42));

// Practical example: Caching with consistent API
let cache = new Map();
function loadCached(url) {
  if (cache.has(url)) {
    return Promise.resolve(cache.get(url)); // Always return promise
  }
  return fetch(url).then(response => {
    cache.set(url, response);
    return response;
  });
}

6. Promise.reject(error)

Purpose: Create an immediately rejected promise
Use Case: Rarely used in practice (async/await preferred)

// These are equivalent
Promise.reject(new Error('Failed'));
new Promise((_, reject) => reject(new Error('Failed')));

Quick Comparison

Method Waits For Resolves When Rejects When
all All to settle All resolve Any rejects
allSettled All to settle Always (never rejects) Never
race First to settle First resolves First rejects
any First to fulfill First resolves All reject

Best Practices

  1. Use Promise.all for parallel operations where you need all results
  2. Use Promise.allSettled when you want all results regardless of failures
  3. Use Promise.race for timeout implementations or fastest-wins scenarios
  4. Use Promise.any when you need the first successful result
  5. Promise.all is most commonly used in real applications
  6. Consider error handling - Promise.all fails fast, others have different behaviors

Error Handling Patterns

Always use .catch() to handle errors in promise chains.

// Pattern 2: Handle partial failures with Promise.allSettled Promise.allSettled([api1(), api2(), api3()]) .then(results => { const successful = results.filter(r => r.status === ‘fulfilled’); const failed = results.filter(r => r.status === ‘rejected’); console.log(${successful.length} succeeded, ${failed.length} failed); });


---


## Async/Await (Promise Syntax Sugar)
Async/await makes working with promises easier and more readable.
```js
async function getUserInfo() {
  try {
    const response = await fetch(githubUserInfoURL);
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error:', error);
  }
}
getUserInfo();

Explanation:


Common Mistakes


Tricky Promises Interview Questions

1. What will be the output?

console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');

Answer: A D C B

Explanation:


2. What does this code print?

Promise.resolve(1)
  .then(x => x + 1)
  .then(x => { throw new Error('Oops!'); })
  .catch(err => {
    console.log(err.message);
    return 42;
  })
  .then(x => console.log(x));

Answer: Oops! 42

Explanation:


3. What will be logged?

const p = Promise.resolve('start');
p.then(() => {
  return p;
}).then(console.log);

Answer: start Explanation:


4. How to run multiple promises in parallel and get all results?

const p1 = Promise.resolve('one');
const p2 = Promise.resolve('two');
Promise.all([p1, p2]).then(console.log);

Answer: [‘one’, ‘two’] Explanation:


5. How to handle both resolved and rejected promises together?

Promise.allSettled([
  Promise.resolve('ok'),
  Promise.reject('fail')
]).then(console.log);

Answer: [ { status: ‘fulfilled’, value: ‘ok’ }, { status: ‘rejected’, reason: ‘fail’ } ] Explanation:


Summary


Further Reading