Why is "this" undefined in this class method?

KarlGdawg picture KarlGdawg · Aug 11, 2017 · Viewed 10.1k times · Source

I've tried to search over what seems to be the entire internet, but I'm still vexed by a problem with a JS class I'm writing for a Micro Service (still in learning a bit).

So, I try to call a class method on an instantiated object, and according to my knowledge and my (faulty I presume) unit tests it should work.

Well, I'll start with the error I receive:

    GET /api/users 500 2.863 ms - 2649
TypeError: Cannot read property 'repository' of undefined
    at list (C:\Users\<user>\Documents\Programming\node\kaguwa-ngn\kaguwa-user-service\controllers\user-controller.js:20:9)
    at Layer.handle [as handle_request] (C:\Users\<user>\Documents\Programming\node\kaguwa-ngn\kaguwa-user-service\node_modules\express\lib\router\layer.js:95:5)
    at next (C:\Users\<user>\Documents\Programming\node\kaguwa-ngn\kaguwa-user-service\node_modules\express\lib\router\route.js:137:13)

(And a lot more).

The code calling code:

user-controller.js

'use strict';

var utils = require('./utils');

class UserController {

  constructor(repository) {
    this.repository = repository || {};
  }

  /**
   * 
   * Lists all users.
   * 
   * @param {object} req 
   * @param {object} res 
   */
  list(req, res) {

    this.repository.list(function (err, users) {
      if (err) return res.status(500).json(utils.createError(500));

      if (Object.keys(users).length !== 0) {
        res.json(users);
      } else {
        res.status(404).json(utils.createNotFound('user', true));
      }
    });
  }
// more code
}

module.exports = UserController

Caller of controller

user-api.js


'use strict';

var express = require('express');
var UserController = require('../controllers/user-controller');

var router = express.Router();

module.exports = function (options) {

  var userController = new UserController(options.repository);

  router.get('/users', userController.list);
  // Mode code

  return router;
};

I really jave no idea on why this undefined in UserController.

Any help would be greatly appriciated.

Answer

jfriend00 picture jfriend00 · Aug 11, 2017

When you do this:

router.get('/users', userController.list);

what gets passed to your router is just a reference to the .list method. The userController instance gets lost. This is not unique to routers - this is a generic property of how things are passed in Javascript. To understand further, what you are essentially doing is this:

let list = userController.list; 
// at this point the list variable has no connection at all to userController
router.get('/users', list);

And, in Javascript's strict mode, when you call a regular function without any object reference such as calling list() above, then this will be undefined inside the function. That is what is happening in your example. To fix it, you need to make sure that your method is called with a proper object reference as in userController.list(...) so that the interpreter sets the this value appropriately.

There are multiple ways to solve this problem:

Make your own function wrapper

router.get('/users', function(req, res)  {
    userController.list(req, res);
});

This works in any version of Javascript.


Using .bind() to make a wrapper for you that calls it with the right object

router.get('/users', userController.list.bind(userController));

This works in ES5+ or with a .bind() polyfill.


Use an ES6 arrow function shortcut

router.get('/users', (...args) => userController.list(...args));

This works in ES6+


Personally, I prefer the .bind() implementation because I think it's just simpler and more declarative/clearer than any of the others and the ES6 "shortcut" isn't really shorter.