Certified Associate JavaScript Programmer study materials
Certified Associate JavaScript Programmer study resources

Cart Total Loses `this` When Passed as a Callback

Certified Associate JavaScript Programmer · Put your knowledge to work in context.

All case studies

Cart Total Loses `this` When Passed as a Callback

A storefront has a cart object with a method that computes a total:

const cart = {
  items: [12, 8, 5],
  taxRate: 0.2,
  total: function () {
    const sub = this.items.reduce((sum, n) => sum + n, 0);
    return sub * (1 + this.taxRate);
  }
};

cart.total() returns 30. But document.querySelector('#total').addEventListener('click', cart.total) throws TypeError: Cannot read properties of undefined (reading 'reduce'). setTimeout(cart.total, 0) fails the same way. A teammate tries rewriting the method as total: () => this.items.reduce(...) and finds that even cart.total() now fails. The team needs a fix that preserves the method's access to the cart and explain why the arrow-function rewrite made the situation worse.

1. Explain why `cart.total()` works while passing `cart.total` as a callback fails.

Read the worked answer

For a normal function, this is determined at call time by how the function is invoked, not by where it was defined. cart.total() is a method call: the receiver is cart, so this is cart and this.items resolves to the array.

Passing cart.total passes only the function object; it does not carry the receiver. The event listener later invokes the function as a plain callback. In strict mode or a module, this is undefined; in sloppy mode it may be the global object. Either way, this.items is not the cart's array, so .reduce fails. setTimeout(cart.total, 0) behaves the same way because the timer calls the function without a receiver.

The method is not permanently bound to cart. Binding happens per call. This is the core judgement call: extracting a method loses its receiver unless you explicitly preserve it.

2. Give two correct fixes and state a tradeoff for each.

Read the worked answer

Fix 1: bind the receiver once and pass the bound function:

const total = cart.total.bind(cart);
document.querySelector('#total').addEventListener('click', total);

This creates a new function whose this is permanently cart. It is clear and works with timers. The tradeoff is that the bound function is a distinct function object, so it cannot be removed with the original cart.total reference; keep the bound reference.

Fix 2: wrap the call in an arrow function at the call site:

document.querySelector('#total').addEventListener('click', () => cart.total());

This preserves the method call syntax, so this is cart. The tradeoff is that the wrapper still evaluates cart at click time; if cart can be reassigned, the handler uses the new cart. A wrapper also creates a new function per registration, so removal needs the same wrapper reference.

A third option is to make the object's state available through a closure-based factory rather than this, but that is a larger design change.

3. Why does the arrow-function rewrite `total: () => this.items.reduce(...)` fail even for `cart.total()`? When is an arrow function appropriate here?

Read the worked answer

Arrow functions do not have their own this; they capture this lexically from the enclosing scope. In an object literal, the enclosing scope is the surrounding module or script, not the object being created. The arrow's this is therefore whatever this was outside the object, commonly undefined in a module. Calling cart.total() cannot change that because arrow functions ignore the call receiver. The failure is not caused by the callback use; the method is already broken for all calls.

An arrow function is appropriate inside a normal method when you need to preserve the method's this across an inner callback:

total: function () {
  return this.items.reduce((sum, n) => sum + n, 0) * (1 + this.taxRate);
}

The outer function receives cart as this; the inner arrow inherits it. Do not use an arrow as the method itself when the method depends on a dynamic receiver.