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.