Mastering JavaScript Promises: Common Pitfalls and Best Practices
This article explains the fundamentals of JavaScript Promises, illustrates typical mistakes such as callback pyramids and missing returns, and demonstrates proper usage of chaining, Promise.all, resolve variations, error handling with catch, and state propagation to write clean, maintainable asynchronous code.
Introduction
During a recent project the author needed to convert synchronous code to asynchronous style and encountered a piece of code that looked suspicious. The code performs a delete operation only after checking existence, returns the result, and handles errors.
// Delete, first check if it exists, then perform the real delete action
function del() {
// Find
return find().then(function(resultOfFind) {
// If not found, return false
if (!resultOfFind) {
return false;
}
// Perform the actual delete
return reallyDelete();
}, function(err) {
// Handle error
handle(err);
});
}
function deleteItem(req, res) {
// Delete
del().then(function(resultOfDelete) {
// Process result or not-found case
res(resultOfDelete);
}, function(err) {
// Handle error
res(err);
});
}The steps are simple: 1) check existence before deletion, 2) if it exists, delete, otherwise return, 3) use the result or error to respond.
Table of Contents
1. Promise basics 2. Promise and the pyramid problem 3. Promise and loops 4. resolve(value) vs resolve(promise) 5. The promise returned by then 6. Promise and error handling 7. Promise state propagation
1. Promise Basics
Promise is an asynchronous programming solution that is more reasonable and powerful than traditional callbacks and events. It was first proposed by the community and standardized in ES6. The article refers to the Promises/A+ specification.
1.1 Common Usage
var promise = new Promise(function(resolve, reject) {
// ... some code
if (/* async operation succeeded */) {
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// success
}, function(error) {
// failure
});Key points: the constructor receives a function with resolve and reject parameters; resolve marks the promise as fulfilled and passes the result; reject marks it as rejected; then binds success and failure callbacks.
1.2 Promise States
Pending (进行中)
Fulfilled (已完成 / Resolved)
Rejected (已失败)
The state can only change from Pending to either Fulfilled or Rejected . Once settled, the result is immutable and any later then callbacks receive the same value immediately.
2. Promise and the Pyramid Problem
When many asynchronous steps depend on previous results, naive nesting creates a “pyramid of doom”. Example of deep nesting:
getUserAdmin().then(function(result) {
if (/* admin */) {
getProjectsWithAdmin().then(function(result) {
getModules(result.ids).then(function(result) {
getInterfaces(result.ids).then(function(result) {
// ...
});
});
});
} else {
// ...
}
});Improved chaining by returning promises:
getUserAdmin().then(function(reult) {
if (/* admin */) {
return getProjectsWithAdmin();
} else {
return getProjectsWithUser();
}
}).then(function(result) {
return getModules(result.ids);
}).then(function(result) {
return getInterfaces(result.ids);
}).then(function(result) {
// ...
});Even cleaner composition:
getUserAdmin()
.then(getProjects)
.then(getModules)
.then(getInterfaces)
.then(procResult);This style is called composing promises and yields clearer structure and easier debugging.
3. Promise and Loops
When deleting all sub‑modules of a project, a common mistake is to call asynchronous deletions inside forEach without waiting for them:
getModules(projectID).then(function(modules) {
modules.forEach(function(module) {
removeModule(module.moduleID);
}); // A – does not wait
}).then(function(result) {
// ...
});The fix is to collect the promises and return Promise.all:
getModules(projectID).then(function(modules) {
var tasks = [];
modules.forEach(function(module) {
tasks.push(removeModule(module.moduleID));
});
return Promise.all(tasks); // note the return
}).then(function(result) {
// ...
}); Promise.allresolves when all promises are fulfilled and rejects as soon as any promise rejects.
4. resolve(value) VS resolve(promise)
Calling resolve with a plain value fulfills the promise with that value. Calling it with another promise adopts that promise’s state. Example:
var d = new Date();
var promise1 = new Promise(function(resolve) {
setTimeout(resolve, 2000, 'resolve from promise 1');
});
var promise2 = new Promise(function(resolve) {
setTimeout(resolve, 1000, promise1); // resolve(promise1)
});
promise2.then(result => console.log('result:', result, new Date() - d));The output shows that promise2 eventually fulfills with the value of promise1, after the longer delay.
The Promise/A+ spec defines how resolution with another promise propagates state and value.
5. The Promise Returned by then
thenalways returns a new promise. This enables chaining. Examples show returning a value, returning another promise, or omitting return (which yields undefined).
var promise = new Promise(function(resolve) {
setTimeout(resolve, 1000, 'resolve from promise');
});
var promise2 = promise.then(function(result) {
console.log(result);
return 'next'; // returning a value
});If a callback returns a promise, the downstream promise adopts that inner promise’s eventual state.
6. Promise and Error Handling
The second argument of then handles rejections, but using .catch (which is then(null, …)) is recommended because it also catches exceptions thrown in the fulfillment handler.
somePromise.catch(function(err) {
// handle error
});
// equivalent to
somePromise.then(null, function(err) {
// handle error
});Differences appear when the fulfillment handler throws; only the .catch form can capture that exception.
7. Promise State Propagation
If a promise has no rejection handler, its rejected reason propagates to the promise returned by then. Example:
var d = new Date();
var p1 = new Promise(function(resolve, reject) {
setTimeout(reject, 1000, 'reject from promise1');
});
var p2 = p1.then(result => {
console.log('p1 then resolve:', result);
});
p2.then(null, function(err) {
console.log('error:', err, new Date() - d);
});The error from p1 reaches p2 because no rejection handler was provided on the first then.
Conclusion
Remember two key points:
Always use return inside promise callbacks to propagate results.
Attach a final .catch to capture any unhandled rejections.
References
Promise/A+ Promise object (Chinese) About promises – how much do you understand?
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
