New JavaScript developers coming from a C# or Java background often run into the problem with Cannot read property X of undefined
when writing Express.js routers as ES6 classes and using dependency injection. In this post I will present two solutions to this, using a support ticketing feature as an example.
My Ticket.controller.ts file below is responsible for exposing the HTTP endpoints for my ticket API. The TicketController
depends on a TicketService
to be injected in the constructor. The ticket service handles all the business logic related to the feature. The business logic is separated from Express so that I can easily switch out Express if neccessary, or use the same logic for other protocols such as web sockets.
The problem
The problem with the code below is that it will result in an error like this:
Cannot read property 'ticketService' of undefined
This is happening because this
is undefined. You can read about how this
works in the MDN Web Docs. In short: the value of this
depends on how the function is called, and in the example code below the function is passed to the router.get() function where the value of this
refers to something else (undefined).
ticket.controller.ts
import { Router, Response, NextFunction } from 'express'
import TicketService from './TicketService';
export default class TicketController {
public router: Router;
private ticketService: TicketService:
constructor(service: TicketService) {
this.router = Router();
this.ticketService = service;
this.routes();
}
private routes() {
this.router.get('/:id', this.read);
}
private async read (req: Request, res: Response, next: NextFunction) {
try {
let ticket = await this.ticketService.get(req.params.id);
res.send(ticket);
} catch(err) {
next(err)
}
}
}
Solution #1: binding ‘this’
The first solution is to bind this
to the read method, which will make the router component instance available wherever the method is sent.
ticket.controller.ts
import { Router, Response, NextFunction } from 'express'
import TicketService from './TicketService';
export default class TicketController {
public router: Router;
private ticketService: TicketService:
constructor(service: TicketService) {
this.router = Router();
this.ticketService = service;
this.routes();
}
private routes() {
this.router.get('/:id', this.read.bind(this));
}
private async read (req: Request, res: Response, next: NextFunction) {
try {
let ticket = await this.ticketService.get(req.params.id);
res.send(ticket);
} catch(err) {
next(err)
}
}
}
By doing so, this
will no longer be undefined, and also reference the TicketComponent-object. The bind-method returns a new function with the value of this
set to the provided argument.
Solution #2: arrow functions
You can also utilize arrow functions to prevent this
from being undefined.
ticket.controller.ts
import { Router, Response, NextFunction } from 'express'
import TicketService from './TicketService';
export default class TicketController {
public router: Router;
private ticketService: TicketService:
constructor(service: TicketService) {
this.router = Router();
this.ticketService = service;
this.routes();
}
private routes() {
this.router.get('/:id', this.read);
}
private read = async (req: Request, res: Response, next: NextFunction) => {
try {
let ticket = await this.ticketService.get(req.params.id);
res.send(ticket);
} catch(err) {
next(err)
}
}
}
Arrow functions keep the enclosing lexical context of this
, which means that this
in the function will reference whatever it referenced where it was defined.
Using arrow functions as properties comes with some caveats. Arrow functions in class properties are not in the prototype, which means other classes cannot inherit the arrow function property. More of these caveats are covered in depth in this article.
Conclusion
Based on the article in the above link, I would recommend using the .bind()-method to solve this problem.