Custom Development

Avoiding "Callback Hell" while Using Mongoose

Sanjar Giyaziddinov

MongoDB is one of the most popular document databases in the NodeJS world. It constantly evolves and has huge community support. As a result, developers created various Object Document Mapper (ODM) tools like Mongoose, Mongolia, Waterline, etc. These ODM tools provide high level abstractions and make our lives a lot easier while working with MongoDB. In this blog post, we are going to discuss Mongoose. We assume that you have basic knowledge about this tool; if not, you can go through the "quick start guide" and detailed documentation at http://mongoosejs.com

Here's a scenario: We have a student and a course and we want to enroll the student into the course. But before enrolling the student, we need to make sure that there are enough seats available in the course. If we follow the official Mongoose documentation then our code should look like this:



  var Student = require('./student.model');
  var Course = rqurie('./course.model');

  //Let's assume we are using Express framework for nodeJs
  function enrollStudent(req, res, next) {

  //First we need to make sure that student exists and load the user.
  Student.findById(req.body.studentId, function(err, student){
      if(err) return next(err);

      //Now we need to load the course
      Course.findById(req.body.courseId, function(err, course){
        if(err) return next(err);

        //Next we need to check if there are available seats in the course
        if(course.isSeatAvailable()){
          //Enroll student into the course and save the course
          course.enrolledStudents.push(student.id);
          course.save(function(err){
            if(err) return next(err);
            return res.json({message : 'Enrollment successful'});
          });
        } else {
          //Call error handler
          return next({message : 'No seats available'});
        }
      });
    }); 
  }

As you can see, even in this simple scenario our function is expanding horizontally because of callbacks. More complicated scenarios will have more callbacks and eventually the code becomes unreadable. One way to solve this problem is by using "promises." If you are not familiar with Promises spec, you can learn more about it at http://promisesaplus.com. In Mongoose 4.x, all queries have an exec() method that returns a promise object. Model.save() method also returns a promise. Let's rewrite our function using promises...

  function enrollStudent(req, res, next) {
    var student;

    //Load the user
    Student.findById(req.body.studentId).exec()

    //Capture student and load the course
    .then(function(studentFromDb){
      student = studentFromDb
      return Course.findById(req.body.courseId).exec();
    })

    //Check if there are available seats in the course
    .then(function(course){
      if(course.isSeatAvailable()){
        //Enroll student into the course
        course.enrolledStudents.push(student.id);
        return course;
      } else {
        //throw an error
        throw new Error('No seats available');
      }
    })

    //Save the course
    .then(function(course){
      return course.save();
    })
    
    //Send the response back
    .then(function(course){
      return res.json({message : 'Enrollment successful'})
    });

    //Catch all errors and call the error handler;
    .then(null, next);

  }

Promise chaining allows us to avoid the "callback hell," and the promise catch block eliminates extraneous error checks. By comparing these two examples, you can see that using promises in Mongoose will make your code a lot cleaner and more readable.

 

Sanjar Giyaziddinov
ABOUT THE AUTHOR

Technical Consultant