On this page

Promises

15 min read TextCh. 4 — Asynchrony

What is a promise?

A promise (Promise) is an object that represents the future result of an asynchronous operation. It can be in one of three states:

  • Pending — The operation has not finished yet
  • Fulfilled — The operation completed successfully (has a value)
  • Rejected — The operation failed (has an error)

Once a promise is resolved or rejected, its state is immutable: it cannot change.

Creating promises

The new Promise() constructor takes a function with two callbacks:

const promise = new Promise((resolve, reject) => {
  // async operation...
  if (success) {
    resolve(result);  // transition to fulfilled
  } else {
    reject(new Error('something failed'));  // transition to rejected
  }
});

Consuming promises

.then()

Receives the value when the promise resolves. It returns a new promise, allowing chaining:

promise
  .then(value => transform(value))
  .then(transformed => useIt(transformed));

.catch()

Catches any error in the chain:

promise
  .then(value => processIt(value))
  .catch(error => handleError(error));

.finally()

Executes always, regardless of whether the promise was resolved or rejected. It is useful for cleaning up resources (closing spinners, releasing locks, etc.).

Chaining promises

Each .then() returns a new promise. If you return a value, the next promise resolves with that value. If you return another promise, the chain waits for it to resolve:

getUser(1)
  .then(user => getProfile(user.id))
  .then(profile => getPhotos(profile.albumId))
  .then(photos => showGallery(photos))
  .catch(error => showError(error));

Promise combinators

JavaScript offers four static methods to work with multiple promises simultaneously:

Method Resolves when... Rejects when...
Promise.all All resolve One fails
Promise.allSettled All finish (success or failure) Never
Promise.race The first finishes The first fails
Promise.any The first succeeds All fail

Pattern: timeout with Promise.race

A common pattern is to race a request against a timer to implement timeouts:

function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), ms)
  );
  return Promise.race([promise, timeout]);
}

Common mistakes

  • Forgetting to return in a .then() — The next promise receives undefined
  • Not handling errors — Always add .catch() at the end of the chain
  • Creating unnecessary promises — If you already have a promise, don't wrap it in another one

Practice

  1. Create and consume a promise: Write a function wait(ms) that returns a promise that resolves after ms milliseconds. Consume it with .then() to print a message when it resolves.
  2. Chain promises with error handling: Create two functions that return promises: getUser(id) and getPosts(userId). Chain them with .then() and add .catch() and .finally() to handle errors and cleanup.
  3. Use Promise.all and Promise.allSettled: Create 3 promises (one that intentionally fails) and run them with Promise.all and then with Promise.allSettled. Compare the results and observe how each combinator handles the failure.

In the next lesson we will learn async/await, the modern way to work with promises.

Always handle errors
Every promise chain should end with .catch() to handle errors. A rejected promise without a catch causes an UnhandledPromiseRejection, which in Node.js terminates the process.
Promise.allSettled vs Promise.all
Use Promise.all when you need ALL promises to succeed. Use Promise.allSettled when you want the results of all of them, even if some fail (e.g., loading data from multiple independent APIs).
javascript
// Creating a promise
function findUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) {
        resolve({ id, name: 'Carlos', email: '[email protected]' });
      } else {
        reject(new Error('Invalid ID'));
      }
    }, 1000);
  });
}

// Consuming with .then/.catch/.finally
findUser(1)
  .then(user => {
    console.log('User:', user.name);
    return user.email;
  })
  .then(email => {
    console.log('Email:', email);
  })
  .catch(error => {
    console.error('Error:', error.message);
  })
  .finally(() => {
    console.log('Search completed');
  });

// Chaining promises (each .then returns a new promise)
function getPosts(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, title: 'First post' },
        { id: 2, title: 'Second post' },
      ]);
    }, 500);
  });
}

findUser(1)
  .then(user => getPosts(user.id))
  .then(posts => console.log('Posts:', posts))
  .catch(error => console.error(error));
javascript
// Simulating async requests with promises
function getUsers() {
  return new Promise((resolve) => {
    setTimeout(() => resolve([{ id: 1, name: 'Ana' }]), 1000);
  });
}

function getPosts() {
  return new Promise((resolve) => {
    setTimeout(() => resolve([{ id: 1, title: 'Hello' }]), 1500);
  });
}

function getComments() {
  return new Promise((resolve) => {
    setTimeout(() => resolve([{ id: 1, text: 'Great!' }]), 800);
  });
}

// Promise.all - waits for ALL (fails if one fails)
Promise.all([getUsers(), getPosts(), getComments()])
  .then(([users, posts, comments]) => {
    console.log('All data:', users, posts, comments);
  })
  .catch(error => {
    console.error('A request failed:', error);
  });

// Promise.allSettled - waits for all (never fails)
Promise.allSettled([getUsers(), getPosts(), getComments()])
  .then(results => {
    results.forEach(r => {
      if (r.status === 'fulfilled') {
        console.log('OK:', r.value);
      } else {
        console.log('Error:', r.reason);
      }
    });
  });

// Promise.race - returns the first to resolve/reject
const timeout = new Promise((_, reject) => {
  setTimeout(() => reject(new Error('Timeout')), 5000);
});

Promise.race([getUsers(), timeout])
  .then(data => console.log('Data:', data))
  .catch(err => console.error(err.message));

// Promise.any - returns the first to RESOLVE
Promise.any([getUsers(), getPosts(), getComments()])
  .then(first => console.log('First successful:', first));