How to Reject a Promise Inside a then() Function in a JavaScript Promise Chain

JavaScript Promises are a powerful tool for handling asynchronous operations, enabling cleaner, more readable code than traditional callbacks. A common pattern is chaining .then() methods to execute sequential async tasks. But what if, after a promise resolves, you need to reject the chain based on a condition (e.g., invalid data, missing permissions, or failed validation)?

In this blog, we’ll explore how to intentionally reject a promise within a .then() handler, ensuring your async flow behaves predictably. We’ll cover the two primary methods to achieve this, provide practical examples, highlight common pitfalls, and share best practices.

Table of Contents#

  1. Understanding Promise Chains
  2. The Role of .then() in Promise Chains
  3. How to Reject a Promise Inside .then()
  4. Practical Examples
  5. Common Pitfalls
  6. Best Practices
  7. Handling Rejections in the Chain
  8. Conclusion
  9. References

Understanding Promise Chains#

A Promise represents a future value (or error) from an asynchronous operation. It can be in one of three states:

  • Pending: Initial state (neither fulfilled nor rejected).
  • Fulfilled: The operation succeeded, returning a value.
  • Rejected: The operation failed, returning an error.

A Promise chain links multiple .then() calls, where each .then() processes the result of the previous promise. Each .then() returns a new promise, allowing sequential execution of async tasks.

For example:

fetchUserData() // Assume this returns a promise  
  .then((user) => processUser(user)) // First .then()  
  .then((processedData) => updateUI(processedData)) // Second .then()  
  .catch((error) => handleError(error)); // Catch errors anywhere in the chain  

The Role of .then() in Promise Chains#

The .then() method accepts two optional callbacks:

  • onFulfilled: Executes when the previous promise resolves (fulfilled state).
  • onRejected: Executes when the previous promise rejects (rejected state).

By default, .then() returns a new promise. The behavior of this new promise depends on what the onFulfilled callback returns:

  • If it returns a non-promise value (e.g., a string, object), the new promise resolves with that value.
  • If it returns a promise, the new promise “adopts” the state of the returned promise (resolves or rejects based on the returned promise).
  • If it throws an error or returns a rejected promise, the new promise rejects with that error.

Our focus is on the third case: making .then() return a rejected promise by explicitly rejecting inside onFulfilled.

How to Reject a Promise Inside .then()#

There are two primary ways to reject a promise chain from within a .then() handler:

Method 1: Throwing an Error#

Throwing an error inside the onFulfilled callback of .then() will cause the new promise returned by .then() to reject with that error. This is the most intuitive approach for synchronous checks.

Example: Throwing an Error#

fetchUserData()  
  .then((user) => {  
    // Check if user is an admin (example condition)  
    if (!user.isAdmin) {  
      // Reject the chain by throwing an error  
      throw new Error("User is not an admin");  
    }  
    return user; // Proceed if condition is met  
  })  
  .then((adminUser) => {  
    console.log("Admin user:", adminUser);  
    return fetchAdminData(adminUser.id); // Next async task  
  })  
  .catch((error) => {  
    console.error("Error in chain:", error.message); // Catches "User is not an admin"  
  });  

Here, if user.isAdmin is false, we throw an error. The .then() returns a rejected promise, skipping the next .then() and jumping to .catch().

Method 2: Returning a Rejected Promise#

Explicitly returning a rejected promise (using Promise.reject()) from onFulfilled also causes .then() to return a rejected promise. This is useful for conditional logic or when working with existing promise-based functions.

Example: Returning Promise.reject()#

fetchUserData()  
  .then((user) => {  
    if (!user.hasPermission) {  
      // Reject by returning a rejected promise  
      return Promise.reject(new Error("User lacks permission"));  
    }  
    return user;  
  })  
  .then((authorizedUser) => {  
    console.log("Authorized user:", authorizedUser);  
  })  
  .catch((error) => {  
    console.error("Rejected:", error.message); // Catches "User lacks permission"  
  });  

Here, Promise.reject(new Error(...)) explicitly returns a rejected promise, identical in effect to throwing an error.

Practical Examples#

Let’s combine both methods in a realistic scenario: validating API response data and rejecting on failure.

Scenario:#

We fetch a product from an API. After resolving, we check if the product is in stock and has a valid price. If either check fails, we reject the chain.

// Simulate API call to fetch product  
const fetchProduct = () => {  
  return Promise.resolve({  
    id: 1,  
    name: "Laptop",  
    inStock: false, // Simulate out-of-stock  
    price: -999, // Invalid price (negative)  
  });  
};  
 
// Chain with validation and rejection  
fetchProduct()  
  .then((product) => {  
    console.log("Fetched product:", product);  
 
    // Check if in stock  
    if (!product.inStock) {  
      throw new Error("Product is out of stock"); // Reject with throw  
    }  
 
    // Check if price is valid  
    if (product.price <= 0) {  
      return Promise.reject(new Error("Invalid product price")); // Reject with Promise.reject()  
    }  
 
    return product; // Proceed if all checks pass  
  })  
  .then((validProduct) => {  
    console.log("Processing valid product:", validProduct);  
  })  
  .catch((error) => {  
    console.error("Validation failed:", error.message); // Output: "Product is out of stock"  
  });  

In this example, the first check (inStock: false) triggers a rejection via throw, so the second check (price) is never reached. The catch block handles the error.

Common Pitfalls#

1. Returning an Error Object (Not Rejecting)#

A common mistake is returning an error object directly (instead of throwing or using Promise.reject()). This causes .then() to resolve with the error object, not reject:

// ❌ This resolves, does NOT reject!  
.then((data) => {  
  if (!data) {  
    return new Error("Data is missing"); // Resolves with Error object  
  }  
})  
.then((result) => {  
  console.log(result); // Logs the Error object (not rejected)  
});  

Fix: Use throw new Error(...) or return Promise.reject(new Error(...)).

2. Forgetting to Handle Rejections#

If you reject a chain but omit a catch() at the end, unhandled promise rejections will occur (visible in the browser console or Node.js terminal):

// ❌ Unhandled rejection!  
fetchUserData().then((user) => {  
  if (!user) throw new Error("User not found");  
});  

Fix: Always add a catch() at the end of the chain to handle errors.

3. Rejecting in onRejected Callbacks#

The onRejected callback (second argument to .then()) is for handling previous rejections. Rejecting here requires explicit action (e.g., rethrowing) to propagate the error further:

// Rejecting in onRejected requires rethrowing  
.then(  
  (data) => process(data),  
  (error) => {  
    console.log("Handling initial error:", error);  
    throw error; // Re-throw to propagate to the next catch()  
  }  
)  
.catch((finalError) => {  
  console.log("Final error:", finalError); // Catches the re-thrown error  
});  

Best Practices#

1. Prefer Throwing for Synchronous Checks#

Use throw for simple synchronous conditions (e.g., validation logic). It’s more readable and aligns with standard error-handling patterns.

2. Use Promise.reject() for Conditional Async Logic#

If you need to conditionally return a promise (e.g., based on an async check), use Promise.reject():

.then((data) => {  
  return data.isValid ? fetchMoreData(data) : Promise.reject(new Error("Invalid data"));  
});  

3. Use Specific Error Types#

Create custom error classes for better error categorization:

class ValidationError extends Error {  
  constructor(message) {  
    super(message);  
    this.name = "ValidationError";  
  }  
}  
 
// Reject with custom error  
throw new ValidationError("Invalid user data");  

This allows granular error handling in catch():

.catch((error) => {  
  if (error instanceof ValidationError) {  
    console.error("Validation failed:", error.message);  
  } else {  
    console.error("Unexpected error:", error);  
  }  
});  

4. Always Terminate Chains with catch()#

A catch() at the end of the chain ensures no unhandled rejections. Even if you expect no errors, it’s safer to include a catch for debugging:

.then(...)  
.then(...)  
.catch((error) => {  
  console.error("Unhandled error in chain:", error);  
  // Optionally re-throw if the error should propagate further  
});  

Handling Rejections in the Chain#

When you reject a .then(), the rejection propagates down the chain until a catch() or a .then() with an onRejected callback handles it. You can even recover from a rejection and resume the chain:

fetchUserData()  
  .then((user) => {  
    if (!user.isActive) {  
      throw new Error("User is inactive");  
    }  
    return user;  
  })  
  .catch((error) => {  
    console.log("Recovering from error:", error.message);  
    return { id: "guest", isActive: true }; // Recover by returning a default user  
  })  
  .then((user) => {  
    console.log("Using user:", user); // Output: "Using user: { id: 'guest', ... }"  
  });  

Here, the catch block recovers from the rejection by returning a default user, allowing the chain to resume.

Conclusion#

Rejecting a promise chain from within .then() is critical for validating data, enforcing conditions, or handling failures in sequential async workflows. The two methods to achieve this are:

  • Throwing an error: Intuitive for synchronous checks.
  • Returning Promise.reject(): Explicit and useful for conditional async logic.

By mastering these techniques and avoiding common pitfalls (e.g., returning error objects directly), you can build robust, predictable promise chains. Always terminate chains with catch() to handle rejections gracefully.

References#