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.

Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Mastering JavaScript Promises: Common Pitfalls and Best Practices

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.all

resolves 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

then

always 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?

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaScriptAsynchronousError HandlingPromisecoding
Tencent IMWeb Frontend Team
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.