0

I would like to try and convert this function into a Promise based function in order to resolve all of these nested callbacks and return warnings from ESLint consistent-return.

Previously, I asked for some help in overcoming an ESLint error with my return statements here as they are not consistent or following the best practices of JS.

My first thought was to simply do return new Promise((resolve, reject) => {...}) inside the remove function, but that would just promisify the whole thing instead of just what's inside the function so I feel like that's not the best way to do this.

Any help appreciated!

function remove(req, res) {
  User.findOne({ username: req.params.username }, (findErr, existingUser) => {
    if (findErr) return res.status(500).send(errorHandler.getErrorMessage(findErr));
    if (!existingUser) return res.status(404).send({ message: 'User not found' });

    existingUser.remove((removeErr) => {
      if (removeErr) return res.status(500).send(errorHandler.getErrorMessage(removeErr));

      return res.json({ message: `${existingUser.username} successfully deleted` });
    });
  });
}
germainelol
  • 3,231
  • 15
  • 46
  • 82

2 Answers2

2

Here's another way you can do it. I started by "promisifying" each findOne and removeUser as separate functions. Then, your route is almost automatically simplified.

There's still some improvements you could make here, but perhaps there's something you can learn from this.

(Thanks @Bergi for the useful recommendations)

const error = (type, message) => Object.assign(new Error(message), {type});
const wrapError = type => err => { throw error(type, errorHandler.getErrorMessage(err));};

const findUser = opts => {
  return new Promise((resolve, reject) => {
    User.findOne(opts, (err, user) => {
      if (err) reject(err);
      else resolve(user);
    });
  }).then(user => {
    if (!user) throw error('USER_NOT_FOUND', 'User not found')
    else return user;
  }, wrapError('USER_FIND_ERROR'));
};

const removeUser = user => {
  return new Promise((resolve, reject) => {
    user.remove(err => {
      if (err) reject(err);
      else resolve();
    });
  }).catch(wrapError('USER_REMOVE_ERROR'));
};

function remove(req, res) {
  findUser({ username: req.params.username })
    .then(removeUser)
    .then(() => res.json({message: `${req.params.username} successfully removed`}))
    .catch(error) => {
      switch (error.type) {
        case 'USER_NOT_FOUND':
          return res.status(404).send(error.message);
        // case 'USER_FIND_ERROR':
        // case 'USER_REMOVE_ERROR':
        default:
          console.error(error.type, error.message, error.stack);
          return res.status(500).send(error.message);
      }
    });
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • I'd recommend to do keep the promisification simple and only use `if (err) reject(err) else resolve(…);`. If you need anything else, such as those `errorHandler.getErrorMessage` calls or the emptyness test, do that in a `then` or `catch` handler. – Bergi Nov 08 '16 at 23:58
  • How about `const error = (type, message) => Object.assign(new Error(message), {type});`? – Bergi Nov 09 '16 at 00:00
  • @Bergi thanks for the suggestions. Can you elaborate on the first comment you made a little bit more? Feel free to edit this answer if that's easier ^_^ – Mulan Nov 09 '16 at 00:28
  • I just did :-) Doing transformations is safer in promise callbacks than inside the plain callback of the promisification. – Bergi Nov 09 '16 at 00:41
  • @Bergi very nice. Thanks for the coop ^_^ – Mulan Nov 09 '16 at 01:47
  • @Bergi Not sure I understand what's happening with the `Object.assign` syntax that you added. By the time it gets to `.catch(error)`, the error itself is `undefined`? – germainelol Nov 09 '16 at 03:17
  • @Bergi My mistake, forgot to add `return` before `Object.assign` after I added `{ }` to the body of the function – germainelol Nov 09 '16 at 03:21
1

Not sure if got your point but you may want to try the following

const findUser = (username) => {
  return new Promise((resolve, reject) => {
    User.findOne({ username }, (error, user) => {
      if (error) {
        reject({ type: 'error', details: errorHandler.getErrorMessage(error) });
        return;
      }

      if (!user) {
        reject({ type: 'not-found', details: { message: 'User not found' } });
        return;
      }

      resolve(user);
    });
  });
};

const removeUser = (username) => {
  return new Promise((resolve, reject) => {
    findUser(username)
      .then(user => {
        user.remove((error) => {
          if (error) {
            reject({ type: 'error', details: errorHandler.getErrorMessage(error) });
            return;
          }

          // Simply resolve on success
          resolve();
        });
      })
      .catch(error => reject(error));
  });
};

function remove(req, res) {
  removeUser(req.params.username)
    .then(() => res.json({ message: `${req.params.username} successfully deleted` }))
    .catch(error => {
      if (error.type === 'not-found') {
        return res.status(404).send(error.details);
      }

      return res.status(500).send(error.details);
    });
}

As you might have noticed above, some of the behaviours have been extracted to functions that return Promises.

Could have possibly been optimised even more but I just wanted to show you what's possible with Promises.

Does that help?

  • Interesting, so using the Promise logic, it would solve both my callback problem and my consistent-return problem. To point me in the right direction, what optimisations did you mean when you said that this could be further optimised? Asking as it looks pretty good to me. – germainelol Nov 08 '16 at 18:03
  • 2
    Avoid the [`Promise` constructor antipattern](http://stackoverflow.com/q/23803743/1048572) in `removeUser`! – Bergi Nov 08 '16 at 18:05
  • Here's a slightly optimised version: https://gist.github.com/maciejsmolinski/c176cb27c5a2d81104acd95ce10715ca – Maciej Smoliński Nov 08 '16 at 18:10
  • And here's what you want to typically do with promises: https://gist.github.com/maciejsmolinski/a0007bfc2369947e1c365c61429d9742 – Maciej Smoliński Nov 08 '16 at 18:12
  • @Bergi totally agree but please notice the nested `user.remove` API call which does not return a promise itself, would have done it differently otherwise – Maciej Smoliński Nov 08 '16 at 18:16
  • Interesting, so all goes well and it runs through the .then calls. Any issues and it gets sent to the .catch call at the end. I'd say it follows the typical promise syntax, but maybe with a couple extra steps due to the Mongoose functions not returning promises and instead just returning the error. – germainelol Nov 08 '16 at 18:30
  • @MaciejSmoliński If you put the `new Promise` (for `user.remove` only) inside the `then` callback, it will be fine. – Bergi Nov 08 '16 at 20:56