coderain blog

Axios 400 Error: Why Your Request Calls 'then' Instead of 'catch'?

If you’ve worked with Axios—one of the most popular HTTP clients for JavaScript—you’ve likely encountered a perplexing scenario: you send a request, the server returns a 400 Bad Request error, and instead of triggering the .catch() block (as you’d expect for errors), the response lands in the .then() block. This behavior can be frustrating, especially when you’re trying to handle errors cleanly.

In this blog, we’ll demystify why a 400 error might bypass .catch() and end up in .then(). We’ll break down Axios’s core promise behavior, explore common pitfalls, and provide actionable solutions to ensure errors are handled as expected.

2026-01

Table of Contents#

  1. Understanding Axios Promise Behavior
  2. What is a 400 Error?
  3. Why 400 Errors Might Trigger then Instead of catch
  4. Debugging the Issue: Key Checks
  5. How to Force 400 Errors into catch
  6. Real-World Examples
  7. Best Practices to Avoid This Pitfall
  8. Conclusion
  9. References

Understanding Axios Promise Behavior#

Before diving into 400 errors, let’s clarify how Axios handles promises. Axios wraps HTTP requests in promises, which can either resolve (success) or reject (error). By default:

  • The promise resolves if the request completes with an HTTP response (regardless of the status code) and the status code passes Axios’s built-in validation.
  • The promise rejects if there’s a network error (e.g., no internet, invalid URL) or if the HTTP status code fails validation.

The critical detail here is Axios’s validateStatus configuration. By default, validateStatus is set to:

validateStatus: (status) => status >= 200 && status < 300

This means Axios considers status codes in the 2xx range (e.g., 200 OK, 201 Created) as "valid," resolving the promise. All other status codes (4xx, 5xx, etc.) are "invalid," causing the promise to reject.

So, why would a 400 Bad Request (a 4xx status) resolve the promise and trigger .then() instead of .catch()? Let’s explore.

What is a 400 Error?#

A 400 Bad Request is an HTTP status code indicating the server cannot process the request due to client-side errors (e.g., malformed JSON, missing required fields, invalid authentication tokens). It’s explicitly a "client error" and should, under normal circumstances, cause Axios to reject the promise.

Why 400 Errors Might Trigger then Instead of catch#

If your 400 error is landing in .then(), one of these scenarios is likely the culprit:

3.1 Custom validateStatus Function#

The most common reason is a custom validateStatus function that explicitly marks 400 errors as "valid." For example, if your Axios config includes:

const axiosInstance = axios.create({
  validateStatus: (status) => {
    // Resolve for 2xx OR 400 status codes
    return status >= 200 && status < 300 || status === 400;
  }
});

Here, validateStatus returns true for 400, so Axios resolves the promise, sending the 400 response to .then().

3.2 Misconfigured Server: 2xx Status with Error Data#

Sometimes, the server misbehaves: it returns a 2xx status code (e.g., 200 OK) but includes error data (e.g., { error: "Bad Request" }). Since 200 passes Axios’s default validateStatus, the promise resolves, and .then() is called—even though the response contains an error.

3.3 Interceptors Converting Rejections to Resolutions#

Axios interceptors let you modify requests/responses globally. A response interceptor might accidentally convert a rejected promise (for 400) into a resolved one. For example:

// Response interceptor that "handles" errors by returning the response
axios.interceptors.response.use(
  (response) => response, // Resolve successful responses
  (error) => {
    // If there's a response (even 400), return it as resolved
    if (error.response) {
      return error.response; // Converts rejection to resolution!
    }
    return Promise.reject(error); // Only reject on network errors
  }
);

Now, a 400 error will trigger the interceptor’s error handler, which returns error.response (a resolved promise). The original request’s .then() will receive this response.

3.4 Using .then(success, error) Instead of .catch()#

JavaScript promises allow two syntaxes for error handling:

  • .then(successHandler, errorHandler)
  • .then(successHandler).catch(errorHandler)

These are not identical. If you use .then(success, error) and the successHandler throws an error, the errorHandler will not catch it. In contrast, .catch() catches errors from both the success handler and the original promise.

However, this is rarely the root cause for 400 errors in .then(), but it’s worth noting if you’re using the two-argument .then() syntax.

Debugging the Issue: Key Checks#

To diagnose why your 400 error is in .then(), follow these steps:

  1. Check the Response Status Code: Log response.status in .then() to confirm it’s truly 400. If it’s 200, the server is misconfigured.

    axios.get("/api/data")
      .then((response) => {
        console.log("Status Code:", response.status); // Is this 400 or 200?
      })
      .catch((error) => console.error("Error:", error));
  2. Inspect Axios Config for validateStatus: Check if validateStatus is customized. Use axiosInstance.defaults.validateStatus to view the current configuration.

  3. Check for Interceptors: List all response interceptors to see if they’re modifying errors:

    console.log(axios.interceptors.response.handlers); // Logs all response interceptors
  4. Verify Server Behavior: Use tools like Postman or curl to send the request directly. Does the server return 400 with a 400 status code, or 200 with error data?

How to Force 400 Errors into catch#

Once you’ve identified the root cause, use these fixes:

  1. Reset validateStatus to Default: Remove custom validateStatus or set it to the default:

    axios.defaults.validateStatus = (status) => status >= 200 && status < 300;
  2. Fix Server Misconfiguration: Ensure the server returns 400 (not 200) for client errors.

  3. Adjust Interceptors to Reject Errors: Modify interceptors to re-throw errors instead of returning them:

    axios.interceptors.response.use(
      (response) => response,
      (error) => {
        // Log the error but don't resolve it
        console.error("Interceptor Error:", error);
        return Promise.reject(error); // Re-throw to trigger .catch()
      }
    );
  4. Use .catch() for Error Handling: Prefer .then().catch() over the two-argument .then() syntax for clarity and reliability.

Real-World Examples#

Example 1: Default Behavior (400 Triggers .catch())#

With default Axios config, a 400 error rejects the promise:

axios.post("/api/submit", { invalid: "data" })
  .then((res) => console.log("Success:", res)) // Not called
  .catch((error) => {
    console.error("Error Status:", error.response.status); // Logs 400
  });

Example 2: Custom validateStatus (400 Triggers .then())#

A custom validateStatus that resolves 400 errors:

const customAxios = axios.create({
  validateStatus: (status) => status === 400 || (status >= 200 && status < 300),
});
 
customAxios.post("/api/submit", { invalid: "data" })
  .then((res) => {
    console.log("Then called with 400:", res.status); // Logs 400
  })
  .catch((error) => console.error("Catch not called")); // Not called

Example 3: Interceptor Converting Rejection to Resolution#

An interceptor that returns error.response, resolving the promise:

axios.interceptors.response.use(
  (res) => res,
  (error) => error.response // Convert rejection to resolution
);
 
axios.post("/api/submit", { invalid: "data" })
  .then((res) => {
    console.log("Then called with 400:", res.status); // Logs 400
  })
  .catch((error) => console.error("Catch not called")); // Not called

Best Practices to Avoid This Pitfall#

  1. Keep validateStatus Default: Only customize validateStatus if you explicitly need to resolve non-2xx statuses (e.g., handling 404 as "empty data" instead of an error).

  2. Use .catch() for Errors: Always pair .then() with .catch() for error handling. Avoid the two-argument .then(success, error) unless you’re certain no errors will propagate from the success handler.

  3. Be Cautious with Interceptors: Ensure response interceptors re-throw errors unless you intentionally want to resolve them (e.g., auto-refreshing tokens for 401 Unauthorized).

  4. Validate Server Responses: Work with backend teams to ensure servers return appropriate status codes (e.g., 400 for client errors, 500 for server errors).

Conclusion#

A 400 Bad Request should, by default, trigger Axios’s promise rejection and land in .catch(). If it’s in .then(), the issue likely stems from a custom validateStatus, misconfigured server, overzealous interceptor, or incorrect error-handling syntax. By debugging the status code, Axios config, and interceptors, you can quickly identify and fix the problem.

Remember: Axios’s behavior is predictable once you understand validateStatus and interceptors. Stick to best practices, and you’ll avoid this common pitfall.

References#