JavaScript Promises: Finally Understanding Them
I’ve been avoiding Promises for too long. Callbacks worked fine, right? Wrong. After refactoring a callback-heavy codebase to use Promises, I’m never going back.
The Problem with Callbacks
Here’s what our code looked like before:
getUser(userId, function(err, user) {
if (err) {
handleError(err);
return;
}
getOrders(user.id, function(err, orders) {
if (err) {
handleError(err);
return;
}
getOrderDetails(orders[0].id, function(err, details) {
if (err) {
handleError(err);
return;
}
// Finally do something with details
processDetails(details);
});
});
});
The dreaded callback pyramid of doom. Error handling is repetitive, and the logic is hard to follow.
The Promise Way
Same code with Promises:
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => processDetails(details))
.catch(err => handleError(err));
Much cleaner. The logic flows top to bottom, and error handling is centralized.
How Promises Work
A Promise represents a value that might not be available yet. It can be in one of three states:
- Pending: Initial state, neither fulfilled nor rejected
- Fulfilled: Operation completed successfully
- Rejected: Operation failed
const promise = new Promise((resolve, reject) => {
// Do async work
if (success) {
resolve(result);
} else {
reject(error);
}
});
promise
.then(result => {
// Handle success
})
.catch(error => {
// Handle error
});
Converting Callbacks to Promises
Most of our code uses Node-style callbacks. Here’s how I wrapped them:
function promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
};
}
// Usage
const getUserPromise = promisify(getUser);
getUserPromise(userId).then(user => console.log(user));
Node 8 will have util.promisify() built-in, but for now, I’m using this helper.
Promise Chaining
The power of Promises is in chaining:
fetch('/api/user')
.then(response => response.json())
.then(user => {
console.log(user.name);
return fetch(`/api/orders/${user.id}`);
})
.then(response => response.json())
.then(orders => {
console.log(orders);
})
.catch(error => {
console.error('Something went wrong:', error);
});
Each .then() returns a new Promise, allowing you to chain operations.
Promise.all for Parallel Operations
When you need to wait for multiple Promises:
Promise.all([
fetch('/api/users'),
fetch('/api/products'),
fetch('/api/orders')
])
.then(([users, products, orders]) => {
// All three requests completed
console.log(users, products, orders);
})
.catch(error => {
// Any request failed
console.error(error);
});
This runs all three requests in parallel and waits for all to complete. Much faster than sequential requests.
Common Mistakes
1. Forgetting to Return
// Wrong - breaks the chain
promise
.then(result => {
doSomething(result); // Forgot to return!
})
.then(result => {
// result is undefined here
});
// Right
promise
.then(result => {
return doSomething(result);
})
.then(result => {
// result is the return value of doSomething
});
2. Nesting Promises
// Wrong - defeats the purpose
promise.then(result => {
return anotherPromise.then(result2 => {
return yetAnother.then(result3 => {
// Back to callback hell!
});
});
});
// Right - chain them
promise
.then(result => anotherPromise)
.then(result2 => yetAnother)
.then(result3 => {
// Clean and flat
});
3. Not Handling Errors
// Wrong - unhandled rejection
promise.then(result => {
// What if this fails?
});
// Right - always catch
promise
.then(result => {
// Handle success
})
.catch(error => {
// Handle error
});
Browser Support
Promises are part of ES6, but not all browsers support them yet. We’re using a polyfill:
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Promise"></script>
Or if you’re using Babel, it includes a Promise polyfill.
What’s Next: Async/Await
ES2017 will have async/await, which makes Promises even cleaner:
async function getOrderDetails(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
return details;
} catch (error) {
handleError(error);
}
}
Looks like synchronous code but is actually asynchronous. Can’t wait for this to land.
Should You Use Promises?
Yes. Absolutely. The benefits are huge:
- Cleaner code
- Better error handling
- Easier to reason about
- Composable
We’ve converted about 80% of our codebase to Promises, and it’s been worth every minute spent refactoring.
If you’re still using callbacks everywhere, give Promises a try. Your future self will thank you.
Resources
What’s your experience with Promises? Any gotchas I missed?