Why do set() traps in proxies sometimes trigger a TypeError and sometimes not (both after an invalid write operation)?

tl;dr: I’ve found that sometimes returning false in a proxy’s set() trap doesn’t trigger any TypeError, and sometimes it does. Why does this happen?


As far as I understand, set() traps in a proxy must return either true or false to denote whether a writing operation was allowed or not. If false is returned, then that should trigger a TypeError.

However, this doesn’t always happen. Take a look at these two examples:

Example 1 (invalid, but no TypeError)

let page = { currentPage: "Home" };

page = new Proxy(page, {
  set(target, property, value) {
    if (typeof value !== "string") return false;
    
    //... additional validation logic could be here

    target[property] = value;
    return true;
  }
});

// Attempt an invalid operation:
page.currentPage = 123;
console.log(page.currentPage); // currentPage is still "Home"
// (writing didn't work, but it didn't trigger any TypeError to let me know!)
  

Example 2 (invalid, and throws TypeError) (example taken from javascript.info)

let numbers = [];

numbers = new Proxy(numbers, { // (*)
  set(target, property, value) { // to intercept property writing
    if (typeof val == 'number') {
      target[prop] = val;
      return true;
    } else {
      return false;
    }
  }
});

numbers.push(1); // added successfully
numbers.push(2); // added successfully
console.log("Length is: " + numbers.length); // 2

// Attempt an invalid operation:
numbers.push("test"); // TypeError ('set' on proxy returned false)

alert("This line is never reached (error in the line above)");

Invalid set operations don’t work (as expected), but why do I sometimes get a TypeError and sometimes not? Ideally, I’d like to always get an error so that I can quickly spot and correct my mistakes. Is the only workaround to manually throw an error whenever I return false in the set() trap?

Thank you very much in advance for any help or insight.

Answer

Returning false from a set trap will only trigger an error when the code that does the assignment runs in strict mode. In non-strict mode a false prevents the assignment but without an error.

In your first piece of code the code runs in non-strict mode only. If you modify it to be strict, you get a TypeError:

"use strict";

let page = { currentPage: "Home" };

page = new Proxy(page, {
  set(target, property, value) {
    if (typeof value !== "string") return false;
    
    //... additional validation logic could be here

    target[property] = value;
    return true;
  }
});

// Attempt an invalid operation:
page.currentPage = 123;
console.log(page.currentPage); // currentPage is still "Home"
// (writing didn't work, but it didn't trigger any TypeError to let me know!)

Different functions or methods might be running either strict mode or not:

let obj = new Proxy(
  { foo: "hello" }, 
  { set() { return false; } }
);

function nonStrict(x) {
  x.foo = 2;
}

function strict(x) {
  "use strict";
  x.foo = 2;
}

nonStrict(obj);   // OK
console.log(obj); // { foo: "hello" }

strict(obj);      // Error
console.log(obj);

In the second block of code it is the .push() method that does the writing. That method apparently runs in strict mode, which is why there is a TypeError when it attempts to write to the array index.

All classes (and other ES6+ constructs like generators, modules, etc) are automatically in strict mode:

class Foo { 
  foo = "hello";
  change() {
    this.foo = "world";
  }
};

const plain = new Foo();
plain.change();     // OK
console.log(plain); // { foo: "world" }

let proxy = new Proxy(
  new Foo(), 
  { set() { return false; } }
);
proxy.change();     // Error
console.log(proxy);

The array is considered a class, hence why its methods are strict.

const arrayLike = { length: 0 };

Array.prototype.push.call(arrayLike, "hello");
console.log(arrayLike); // { 0: "hello", length: 1 }

let proxy = new Proxy(
  arrayLike, 
  { set() { return false; } }
);
Array.prototype.push.call(proxy, "world"); // Error
console.log(arrayLike);