Search code examples
javascriptwindowdescriptoronerror

Why doesn't the `window.onerror` getter trigger when an uncaught error occurs?


I want to interceptor the window.onerror which user defined, and get the error parameters info to do some analysis. The following is a part of the interceptor code:

const userError = window.onerror;
delete window.onerror;

const errorFn = (...args) => {
  // some code to collect args info
  if (userError) {
    userError.apply(window, args)
  }
}

Object.defineProperty(window, 'onerror', {
  get() {
    console.log('ONERROR GETTER');
    return errorFn
  },
  set() {
    // ... don't care
  }
})

However, the getter would not execute when an error occurs, such as calling window.abcdefg() which does not actually exist in window. Additionally, it does not print the identifier "ONERROR GETTER", which seems strange to me 😭...

I have shared my question and requested help. It is currently understood that the DOM is not a standard JavaScript object, and the behavior of onerror is not defined in the specification.

Now, I am sharing my question and hoping to receive more opinions.


Solution

  • TL;DR: Why do you need Object.defineProperty()? Just wrap your "interceptor" around the user's event handler, and reassign it like so:

    const userError = window.onerror;
    window.onerror = (...args) => {
      // some code to collect args info
      userError?.apply(window, args)
    }
    

    I'm going to give a highly speculative answer based on my own tests because I couldn't find much information from browser source code, MDN documentation, or the HTML spec:

    So as you probably know, onerror is an attribute event listener, which enables you to conveniently get and set an event handler for "error" events, instead of using addEventListener().

    But did you know that the onerror property of window is by default an accessor property, meaning its descriptor defines getters and setters rather than a straightforward value. We can confirm this by inspecting Object.getOwnPropertyDescriptor(window, "onerror"), and this seems to be consistent across browsers.

    So, this actually might hint towards how window.onerror implements attaching the event listener to window. That is, the set() accessor of onerror effectively just calls addEventListener() on the new value, and removeEventListener() on the old value, but in native code. However, we can easily reimplement this behavior in JavaScript like so:

    // For the sake of interactivity, I'm showing 'onclick'
    let value = undefined;
    Object.defineProperty(window, 'onclick', {
      get() {
        return value;
      },
      set(newValue) {
        console.log("new click handler"); // proof that our set() works
        window.removeEventListener('click', value);
        value = newValue;
        window.addEventListener('click', value);
      }
    });
    
    // demo
    window.onclick = () => console.log("hello");
    document.body.click();
    window.onclick = () => console.log("world");
    document.body.click();

    So what does this entail? Well it means that when the window receives an "error" event (or any other attribute event), the browser isn't performing some special case to call window.onerror(). Instead, the browser likely uses the same logic as running event handlers attached directly with addEventListener(). In other words, the browser never actually accesses window.onerror, and that's why your get() accessor doesn't execute and "ONERROR GETTER" isn't printed.

    This also explains how attribute event listeners respect definition order when multiple event listeners of the same type are defined; attribute event listeners are essentially just syntactic sugar for calling addEventListener().

    As a result, if you're overriding the descriptor with Object.defineProperty(), you will need to write something similar to the code above to preserve the semantics of onerror. However, see the TL;DR for how you can achieve what you probably want much more simply.