
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.