This rule raises an issue when a .then() or .catch() callback itself contains another .then() or .catch() call instead of continuing the outer chain.

Why is this an issue?

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;
  });

Exceptions

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);
}

How to fix it

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.

Resources

Documentation