Advanced30 min read

Promises

Understand asynchronous JavaScript with Promises — creating, chaining, and handling success and failure states.

Why Asynchronous?

JavaScript is single-threaded — it can only execute one piece of code at a time. If a long-running operation (like a network request or a timer) ran synchronously, the entire page would freeze until it finished.

Asynchronous programming solves this by letting code continue running while waiting for slow operations to complete. The result is delivered later, when it is ready.

Callbacks were the original solution:

javascript
setTimeout(function() {
  console.log('Done after 1 second');
}, 1000);
console.log('This runs immediately');

But callbacks quickly lead to deeply nested, hard-to-read code known as "callback hell":

javascript
getUser(id, function(user) {
  getOrders(user, function(orders) {
    getDetails(orders[0], function(details) {
      // 3 levels deep and growing...
    });
  });
});

Promises were introduced in ES6 to solve this problem by providing a cleaner, chainable API for asynchronous operations.

Promise Basics

A Promise is an object that represents a value that may not be available yet but will be at some point in the future (or will fail).

A Promise has three possible states:

  • Pending — the initial state; the operation has not completed yet.
  • Fulfilled — the operation completed successfully and produced a value.
  • Rejected — the operation failed and produced a reason (an error).

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

Creating a Promise:

javascript
const myPromise = new Promise((resolve, reject) => {
  // Do some async work...
  const success = true;
  if (success) {
    resolve('It worked!');  // Fulfill the promise
  } else {
    reject('Something failed');  // Reject the promise
  }
});

Consuming a Promise:

javascript
myPromise
  .then(value => console.log(value))   // 'It worked!'
  .catch(error => console.log(error))  // handles rejection
  .finally(() => console.log('Done')); // runs either way
  • .then(onFulfilled) — called when the Promise is fulfilled.
  • .catch(onRejected) — called when the Promise is rejected.
  • .finally(onSettled) — called when the Promise is settled (fulfilled or rejected), useful for cleanup.

Promise Chaining

One of the most powerful features of Promises is chaining. Each .then() call returns a new Promise, allowing you to chain multiple asynchronous steps in sequence.

javascript
fetch('/api/user')
  .then(response => response.json())   // Parse JSON
  .then(user => fetch('/api/orders/' + user.id)) // Fetch orders
  .then(response => response.json())   // Parse JSON
  .then(orders => console.log(orders)) // Use the data
  .catch(error => console.error(error)); // Handle ANY error

Key rules of chaining:

  1. Returning a value from .then() wraps it in a resolved Promise automatically:
javascript
Promise.resolve(1)
  .then(n => n + 1)   // Returns 2, wrapped in Promise.resolve(2)
  .then(n => n * 3)   // Returns 6
  .then(n => console.log(n)); // 6
  1. Returning a Promise from .then() is awaited before the next .then() runs.

  2. Errors propagate down the chain to the nearest .catch():

javascript
Promise.resolve('start')
  .then(() => { throw new Error('Oops'); })
  .then(() => console.log('Skipped'))  // Skipped!
  .catch(err => console.log(err.message)); // 'Oops'

This flat, readable chain replaces the nested callback pattern entirely.

Promise Utilities

JavaScript provides several static methods on the Promise constructor for working with multiple Promises at once.

Promise.all(iterable) — waits for all Promises to fulfill. If any rejects, the whole result rejects:

javascript
Promise.all([fetchUser(), fetchOrders(), fetchSettings()])
  .then(([user, orders, settings]) => { /* all done */ })
  .catch(err => { /* one failed */ });

Promise.race(iterable) — resolves or rejects with the first Promise to settle:

javascript
Promise.race([fetchData(), timeout(5000)])
  .then(data => console.log(data))
  .catch(() => console.log('Timed out'));

Promise.allSettled(iterable) — waits for all Promises to settle (no short-circuit on rejection). Each result has { status, value } or { status, reason }:

javascript
Promise.allSettled([p1, p2, p3])
  .then(results => {
    results.forEach(r => console.log(r.status)); // 'fulfilled' or 'rejected'
  });

Promise.any(iterable) — resolves with the first Promise to fulfill. Only rejects if all reject (with an AggregateError):

javascript
Promise.any([fetchFromCDN1(), fetchFromCDN2()])
  .then(data => console.log('Got data from fastest CDN'));

Promise in Action

html
<div id="output"></div>

<script>
  // Simulate an async operation with setTimeout + Promise
  function fetchUser(id) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (id > 0) {
          resolve({ id: id, name: 'Alice' });
        } else {
          reject('Invalid user ID');
        }
      }, 500);
    });
  }

  const output = document.getElementById('output');
  output.innerHTML = '<p>Loading...</p>';

  fetchUser(1)
    .then(user => {
      output.innerHTML = `<p>User: ${user.name} (ID: ${user.id})</p>`;
      return user.name.toUpperCase();
    })
    .then(upperName => {
      output.innerHTML += `<p>Uppercase: ${upperName}</p>`;
    })
    .catch(error => {
      output.innerHTML = `<p>Error: ${error}</p>`;
    })
    .finally(() => {
      output.innerHTML += '<p>Request complete.</p>';
    });
</script>

What are the three states of a Promise?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.