Tracking down where a property changes in JavaScript

2020-02-22

Without strict enforcement of boundaries, it can be a mystery where changes happen in the state in your JavaScript application (this is a reason tools like Redux are compelling). I was recently investigating a bug in an app where the source of a state change was a mystery. The app keeps track of the count of videos uploaded. Deleting a video could sometimes cause the count to decrement twice when it should only decrement once. I would be able to find this bug quickly if I could get a stack trace every time the state was modified.

This isn't a new problem. You may remember the promise of obj.watch and Object.observe() before they were deprecated.

Let's talk about some current options.

Proxy

Proxy allows you to create a replacement for your object that wraps the original object. You can define your own set "trap" to log the stack trace (or break into a debugger, etc.).

example:

let proxy = new Proxy(objectYouWantToObserve, {
  set: function(obj, prop, value) {
    if (prop === 'thePropertyWeWantToObserve') {
      console.trace(`set: ${prop} -> ${value}`)
    }

    // The default behavior to store the value
    obj[prop] = value;

    // Indicate success
    return true;
  }

  // you can optionally define an additional trap for `get` if you're also
  // interested in tracking reads for the property
});

You would then need to use your proxy instance wherever you would have used objectYouWantToObserve.

The downside of this approach is that, depending on your app design, it isn't always convenient to replace references to the original object.

breakOn

Paul Irish's library break-on-access exposes a breakOn function that will happily invoke the debugger whenever a property is modified (or, optionally, accessed for read).

example:

breakOn(objectYouWantToObserve, "thePropertyWeWantToObserve")

The upside of this approach is that it requires no additional modification to your code. Any changes to thePropertyWeWantToObserve will invoke the debugger.

I keep break-on-access in my Chrome snippets and reach for it first when I need to track down a change in and object's top-level property.

What if I need to observe changes in dynamically nested objects?

If you need to observe properties changing in nested objects, break-on-access won't help you. With some effort, you can use nested Proxys by returning a new Proxy from the Proxy's set trap. I'd also consider something like Observable Slim since it allows observing even deeply-nested changes to an object and its usage is similar to that of Proxy.

My goofy face

Hi, I'm Jeffrey Chupp.
I solve problems, often with code.