Search code examples
node.jshttppromisees6-promise

The success block of my promise always executes even when I return a 404


When a user logs in with incorrect email and password, the success block of my client-side promise still executes, even though the server returned a 400.

I'm using Redux with React so I'm calling an action creator which calls an HTTP request using axios. I need help understanding why I'm not handling errors correctly, because the rest of my app's auth functions like signup, logout, etc all behave the same way even though I'm returning 400 statuses from the server.

Here is where I call login from my component, the success block always executes:

handleFormSubmit({
  email, password
}) {
  this.props.loginUser({
    email, password
  }).then(() => {
    toastr.success("You are logged in!");
  }).catch(() => {
    toastr.warning("Could not log in");
  })
}

Here is the action creator "loginUser", the success block for this function does NOT run when I return a 400 from the server:

export function loginUser({ email, password }) {

  return function(dispatch) {

    return axios.post(`/users/login`, {
        email, password
      })
      .then(response => {

        dispatch({
          type: AUTH_USER
        });


        localStorage.setItem('token', response.headers['x-auth']);
        browserHistory.push('/feature');
      })
      .catch(() => {
        dispatch(authError('Incorrect email or password'));
      });
  }
}

Here is the route '/users/login' Please note that the 400 status does in fact return:

app.post('/users/login', (req, res) => {
  var body = _.pick(req.body, ['email', 'password']);

  User.findByCredentials(body.email, body.password).then(user => {
    return user.generateAuthToken().then(token => {
      res.header('x-auth', token).send(user);
    });
  }).catch(e => {
    res.status(400).send();
  });
});

Solution

  • You issue is that you're misunderstanding what a catch clause is in promises.

    The way you can think if it is just a then with a rejection handler:

    .then(null, function(err) {
      // handle the error
    })
    

    Meaning it only handles the last unhandled error from the promise chain, and you can continue chaining after it no matter what happened.

    Example:

    new Promise((resolve, reject) => {
      setTimeout(() => reject(Error('After 1 sec')), 1000)
    })
    .catch((err) => {
      console.log(`catch: ${err}`);
      return 5;
    })
    .then((five) => {
     // this chains because the error was handled before in the chain
     console.log(`catch.then: ${five}`); // 5
    })
    .catch(() => {
      console.log('No error happened between this error handler and previous so this is not logged');
    });

    To make an error propagate from the current catch to the next error handler, you can return a rejected Promise (or re-throw the error) to make the chain skip all the success handlers until the next fail (or catch) handler.

    new Promise((resolve, reject) => {
      setTimeout(() => reject(Error('After 1 sec')), 1000)
    })
    .catch((err) => {
      // return a reject promise to propagate to the next error handler
      return Promise.reject(err);
      // can also `throw err;`
    })
    .then((nothing) => {
     // this doesn't happen now
     console.log(nothing);
    })
    .catch(console.error); // this logs the error

    Side note: When you don't provide a rejection handler in a Promise chain (the second parameter in .then), the default rejection handler essentially behaves like:

    function defaultRejectionHandler(err) {
      throw err;
    }
    

    Which means it re-throws whatever error was passed into it so the error can propagate to the next error handler that you do specify.