This rule raises an issue when a .then() or .catch() callback itself contains another .then() or
.catch() call instead of continuing the outer chain.
Promises exist to turn asynchronous callbacks into a flat, linear chain: each .then() or .catch() takes the result of the
previous step and hands back a new promise for the next one. Calling .then() or .catch() inside another
.then()/.catch() callback, instead of returning the inner promise and chaining onto the outer one, breaks that flatness and
reintroduces the "pyramid of doom" that promises were designed to
avoid.
Nested promises are harder to follow than a flat chain: the reader has to track two levels of asynchronous control flow instead of one, and the
indentation grows with every additional step. Nesting also makes it easy to forget to return the inner promise, which silently detaches
it from the outer chain — the outer .then() resolves without waiting for the inner asynchronous work to finish, and any rejection from
the inner promise is no longer caught by the outer chain’s .catch().
authenticate(token).then(function () {
return checkPermissions(userId).then(function () { // Noncompliant: nested promise
return logAudit(userId).then(function () { // Noncompliant: nested promise
return grantAccess(userId);
});
});
});
authenticate(token)
.then(function () {
return checkPermissions(userId);
})
.then(function () {
return logAudit(userId);
})
.then(function () {
return grantAccess(userId);
});
The same applies to .catch():
fetchUser(userId)
.catch(function (error) {
return logError(error).catch(function () { // Noncompliant: nested promise
return null;
});
});
fetchUser(userId)
.catch(function (error) {
return logError(error);
})
.catch(function () {
return null;
});
Nesting is not flagged when the inner call needs a variable that only exists in the outer callback’s scope: flattening the chain would make that variable unreachable where it is needed.
fetchUser(userId).then(user => fetchPosts(user.id).then(posts => renderProfile(user, posts)) // Compliant: "user" is needed by renderProfile );
Even though this nesting is not flagged, it can still be avoided by rewriting the code with async/await, which keeps
every step’s data in scope without nesting:
async function renderUserProfile(userId) {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
return renderProfile(user, posts);
}
Return the inner promise instead of chaining onto it, and continue with another .then()/.catch() on the outer chain:
fetchUser(userId) .then(user => fetchPosts(user.id)) .then(posts => renderPosts(posts));
When a later step needs a value produced by an earlier one, as in the exception above, extract the dependent step into a named function that carries that value forward alongside its own result, instead of nesting the callback inline to keep it in scope. This keeps the chain flat while still giving every step access to the data it needs:
function withPosts(user) {
return fetchPosts(user.id).then(posts => ({ user, posts }));
}
fetchUser(userId)
.then(user => withPosts(user))
.then(({ user, posts }) => renderProfile(user, posts));
In rare cases — for example, retrying an operation from inside a rejection handler — the nested call cannot be flattened without changing the code’s behavior. When that happens, it is acceptable to keep the nesting and suppress the issue for that line rather than force an artificial refactoring.