Mastering Promises in JavaScript


When you first encounter promises in JavaScript, they can seem a little abstract. Why are they called promises? What’s actually being promised? And how are they better than callbacks?

Let’s break this down.

What is a Promise?

A promise is an object representing the eventual completion or failure of an asynchronous operation. Think of it as a placeholder for a value that hasn’t arrived yet but will in the future.

It has three possible states:

  • Pending – it hasn’t completed yet
  • Fulfilled – it completed successfully
  • Rejected – it failed

Here’s an analogy that makes this more intuitive.

Imagine you ask a friend, Ben, to pick up a birthday cake. He agrees. That agreement is a promise. He hasn’t delivered the cake yet – so the promise is still pending. If he brings it, the promise is fulfilled. If he forgets, the promise is rejected.

const promise = benBuysCake('chocolate')

And here’s how you handle the result:

benBuysCake('chocolate')
  .then(partyAsPlanned)
  .catch(buyCakeYourself)

Why Use Promises?

Promises help you write asynchronous code without deeply nested callbacks. You gain:

  • Cleaner flow of async operations
  • Centralized error handling
  • Chained operations with less boilerplate

Compare these two versions of async code:

Callback-based:

chargeCustomer(customer, (err, charge) => {
  if (err) return handleError(err)
  addToDatabase(customer, (err, result) => {
    if (err) return handleError(err)
    sendEmail(customer, (err) => {
      if (err) return handleError(err)
      res.send('Success!')
    })
  })
})

Promise-based:

chargeCustomer(customer)
  .then(() => addToDatabase(customer))
  .then(() => sendEmail(customer))
  .then(() => res.send('Success!'))
  .catch(handleError)

The promise version is easier to follow and scales better as complexity grows.

How to Construct a Promise

To create a promise, use the Promise constructor:

const promise = new Promise((resolve, reject) => {
  // Do async work
  if (success) {
    resolve(data)
  } else {
    reject(error)
  }
})

Example:

const benBuysCake = (cakeType) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (cakeType === 'chocolate') {
        resolve('chocolate cake!')
      } else {
        reject('No cake 😢')
      }
    }, 1000)
  })
}

Now use it:

benBuysCake('chocolate')
  .then(cake => console.log(cake))
  .catch(err => console.log(err))

Running Multiple Promises at Once

When you need to run several async operations in parallel, use Promise.all:

const fries = getFries()
const burger = getBurger()
const drink = getDrink()

Promise.all([fries, burger, drink])
  .then(([fries, burger, drink]) => {
    console.log(`Burger: ${burger}, Fries: ${fries}, Drink: ${drink}`)
  })
  .catch(console.error)

All promises must resolve for the then to run. If any fail, it goes straight to catch.

Browser Support

Modern browsers support Promises natively. For older browsers like IE11, a polyfill such as es6-promise works well.

Conclusion

Promises are now a fundamental part of JavaScript. They make async logic more readable, less error-prone, and easier to debug. If you’ve used callbacks in the past, switching to Promises is a worthwhile upgrade.

That said, callbacks still have their place – especially in low-level APIs. But when writing modern JavaScript, Promises are a safe bet.