node.jsexpresspromisesequelize.jsbluebird

Express 4 - chaining res.json with promise.then does not work


I'm working on an express 4 app that uses mysql and sequelize packages. Sequelize ORM uses promises to fetch data from database. I'm trying to fetch data in router and send json response. When I try to chain then callback of promise with res.json I get an error in console saying Unhandled rejection TypeError: Cannot read property 'get' of undefined

// This works
employeeRouter.get("/:id", function(req, res){
   Employee.findById(req.params.id).then(function(data){
      res.json(data);
   });
});

// Replacing above code with following doesn't work
employeeRouter.get("/:id", function(req, res){
   Employee.findById(req.params.id).then(res.json);
});

Error Stack:

Unhandled rejection TypeError: Cannot read property 'get' of undefined
    at json (D:\Workstation\DataPro\CountryStats\node_modules\express\lib\response.js:241:21)
    at tryCatcher (D:\Workstation\DataPro\CountryStats\node_modules\bluebird\js\release\util.js:16:23)
    at Promise._settlePromiseFromHandler (D:\Workstation\DataPro\CountryStats\node_modules\bluebird\js\release\promise.js:504:31)
    at Promise._settlePromise (D:\Workstation\DataPro\CountryStats\node_modules\bluebird\js\release\promise.js:561:18)
    at Promise._settlePromise0 (D:\Workstation\DataPro\CountryStats\node_modules\bluebird\js\release\promise.js:606:10)
    at Promise._settlePromises (D:\Workstation\DataPro\CountryStats\node_modules\bluebird\js\release\promise.js:685:18)
    at Async._drainQueue (D:\Workstation\DataPro\CountryStats\node_modules\bluebird\js\release\async.js:138:16)
    at Async._drainQueues (D:\Workstation\DataPro\CountryStats\node_modules\bluebird\js\release\async.js:148:10)
    at Immediate.Async.drainQueues [as _onImmediate] (D:\Workstation\DataPro\CountryStats\node_modules\bluebird\js\release\async.js:17:14)
    at processImmediate [as _immediateCallback] (timers.js:383:17)

models/employee.js

var Sequelize = require('sequelize'),
    sequelize = require('../db-connect/sequelize');

(function(){

  // Use Strict Linting
  'use strict';

  // Define Sequalize
  var Employee = sequelize.define('employee', {
    empNo: { field: 'emp_no', type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },
    birthDate: { field: 'birth_date', type: Sequelize.DATE },
    firstName: { field: 'first_name', type: Sequelize.STRING },
    lastName: { field: 'last_name', type: Sequelize.STRING },
    gender: { field: 'gender', type: Sequelize.ENUM('M', 'F') },
    hireDate: { field: 'hire_date', type: Sequelize.DATE },
  });

  // Export
  module.exports = Employee;

}());

db-connect/sequelize.js

var Sequelize = require('sequelize');

(function(){

  // Use Strict Linting
  'use strict';

  // Sequalize Connection
  var sequelize = null;

  // Create Sequalize Connection
  if(!sequelize){
    sequelize = new Sequelize('employees', 'root', '', {
      host: 'localhost',
      dialect: 'mysql',
      define: {
        timestamps: false
      }
    });
  }

  module.exports = sequelize;

}());

routes/employees.js

var express = require('express'),
    Employee = require('../models/employee');

(function(app){

  // Use Strict Linting
  'use strict';

  // Create Router
  var employeeRouter = express.Router();

  // Home Page
  employeeRouter.get("/", function(req, res){
    res.json({employees: ['all']});
  });

  // Get Specific Employee
  employeeRouter.get("/:id", function(req, res, next){
    Employee.findById(req.params.id).then(function(data){
      res.json(data);
    });
  });

  // ----------------------------------
  // Export
  // ----------------------------------

  module.exports = employeeRouter;

}());

Solution

  • When you pass res.json as a function, the res object gets lost and thus when json() executes, it has no object and you get the error you are seeing. You can fix that by using .bind():

    employeeRouter.get("/:id", function(req, res){
       Employee.findById(req.params.id).then(res.json.bind(res));
    });
    

    This will make sure that the res object stays with your method when the method is executed. Using .bind() as above is essentially the same as:

    employeeRouter.get("/:id", function(req, res){
       Employee.findById(req.params.id).then(function(data) {
           return res.json(data);
       });
    });
    

    In fact, .bind() actually creates a stub function like the anonymous one in the above example. It just does it for you rather than making you do it.


    To further example, let's say you had two separate res objects, res1 and res2 from two separate requests.

    var x = res1.json;
    var y = res2.json;
    
    console.log(x === y);    // true, no association with either res1 or res2 any more
    

    This is because referencing res1.json just gets a reference to the .json method. It uses res1 to get that method (which is fetched from the res1 prototype, but once it has the method, it's just a pointer to the method and there is no longer an association with the object that contained the method. So, when you pass res.json to a function, you get no attachment to res. Then when the function you passed res.json to goes to actually call your function it calls it like this:

    var z = res.json;
    z();
    

    And, when z() is called, the this value inside of json ends up being undefined and there is no connection to the res object. Using .bind() creates a stub function that calls it as res.json(...) to keep the connection to the object and make sure this is set properly when the method is executed.