JS Proxy Shenanigans

Andrea Giammarchi
4 min readNov 20, 2023

--

This is a quick walk through all the things that might be unexpected while dealing with the Proxy standard as presented, and proudly so, by ECMAScript.

The non-existent typeof expectation

By design, proxies can deal only with non-primitive values, except function and objects. However, there’s a hidden quite-primitive value to deal with, which is a non typed Array, as typed work as expected, and this is why:

const wut = new Proxy([], {
ownKeys: () => []
});

Object.keys(wut);

// Uncaught TypeError:
// 'ownKeys' on proxy: trap result
// did not include 'length'

… and what’s the issue there?” well, the issue is that a generic proxy handler should account for typeof target === “object” && target !== null && Array.isArray(target) within the handler to eventually return non-breaking runtime code … how user friendly is that?

The non-existent isArray trap

Following the previous example, the shenanigans are not only on array references ’cause, if you wrap your array or value into an object, other things will go wrong:

const wut = new Proxy({value: [1, 2, 3]}, Reflect);

Array.isArray(wut);

// false

The issue here is that if we’d like to pretend any non array value is an array, we’ll fail, but when we actually use an array as trap, we won’t survive the isArray check as that would be true… so that using arrays as unique way to deal with Proxies is also not a good idea, as I’ve recently learned in one of my most complex projects to date.

That project also needed to monkey patch the global isArray to provide reliable results as it’s impossible to have just a typeof "object" in the mix and make all traps and native calls behavior be good with each other.

Apply and Construct want functions

This is an easy one to maybe understand: if your proxied value is not a function, apply or construct traps won’t work:

const wut = new Proxy({}, {
apply() {},
construct() {}
});

new wut();

// Uncaught TypeError:
// wut is not a constructor

TC39 members would die on the hill that justifies this behavior, but to me everything I’ve encountered so far is just an odd spec based on the target your are proxying instead of one based on the traps you are providing … I mean: why doesn’t that new Proxy operation throws already if it encounters traps it’s not meant to handle, instead of breaking potentially production code out of the blue and too late on the pipe? 🤦 🤷

I can hear the “because of generic handlers meant to deal with all cases” as excuse in there but then again, surprises are the only things that land in production to me. It’s trivial enough to define ad-hoc handlers for typeof "function" VS handlers for typeof "object" in our daily code, this was a pure “delegate to user-land” shenanigan to me.

The Proxy I Dream About

After dealing with Python long enough to appreciate all its magic handlers about all proxied things, I don’t want JS to strictly mimic all of that but I feel like a better Proxy primitive is missing in the language:

  • the Proxy premise is that nobody should understand if a reference, not a primitive, is a proxy or not … yet instanceof opertaor, among Array.isArray and both apply and constructor traps, leak the original intent of a value … so that all of these shenanigans are acceptable to TC39: (Array.isArray(p) && !(p instanceof Array) or Object.keys(p).length === 10 && p.length === undefined or typeof p === 'function' && p() /* throw a non callable error */ … all of this is fine and perfect for TC39, but by no mean acceptable for developers … strawberry on top, it’s impossible to proxy DOM nodes on the Web and pretend these will work out of the blue as every single API based on JS drills the “non discoverable” Proxy nature and respectfully fucks everything up on demand
  • the current Proxy API is screaming for a disambiguation trap … as the instanceof dedicated one has not been internally used to break out of the blue for traps meant to simply carry developers intended values … the instanceof trap fails to disambiguate for functions, arrays, or anything else, that’s used internally in the proxy resolution steps .. how bizarre … they care about not revealing yet there are holes in that specs that reveal all sort of issues …
  • the Proxy I am dreaming about does not relate anything to the target and it simply acts like a proxy so that the membrane is really transparent but we can intercept any foreign operation to that membrane so that ownKeys returns just the keys we want to return, apply or construct just pass to the proxy traps or fail if these are absent, and so on for any other available trap in the specs.

That’s it … Proxy is half-doomed while opening tons of interesing, not purposely designed, use cases these days, but they lack a lot of real-world scenarios, developers intents, and expectations, so I hope this post helped you at least a bit.

--

--

Andrea Giammarchi

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.