What this usually means
The most common cause is that a trap function does not call the corresponding Reflect method, or it calls it but discards the return value. Every trap must return the correct value to mimic default behavior. For example, the get trap must return Reflect.get(...) to forward the operation. If you return undefined or a custom value, the proxy will behave differently. Another frequent issue is accidentally creating a revoked proxy or using a proxy as a receiver incorrectly, leading to TypeError or silent failures.
The first ten minutes — establish facts before touching code.
- 1console.log each trap's arguments and return value to see what's happening
- 2Check if your proxy is revoked by calling Proxy.revocable and then revoke()
- 3Verify that every trap that should forward to target uses Reflect.<trap>(...args) and returns its result
- 4Test the proxy in strict mode by adding 'use strict' to see if errors surface
- 5Add a 'get' trap that returns Reflect.get(...) and see if the symptom disappears
The specific files, logs, configs, and dashboards that usually own this bug.
- searchThe proxy handler object – check each trap definition
- searchThe target object – verify properties exist and are enumerable/configurable
- searchConsole output for stack traces, especially 'max call stack size'
- searchDeveloper Tools Sources panel – set breakpoints inside trap functions
- searchThe Reflect API documentation to ensure correct parameters for each trap
- searchAny code that calls Proxy.revocable – check if .revoke() was called inadvertently
Practical causes, not theory. These are the things you will actually find.
- warningTrap function missing return statement or returning undefined instead of Reflect.<trap>(...)
- warningUsing a revoked proxy (calling revoke() on a Proxy.revocable instance)
- warningInfinite recursion when a trap uses the proxy itself instead of the target
- warningNot handling non-configurable or non-writable properties correctly in traps
- warningStrict mode differences: some traps behave differently (e.g., set returns boolean)
- warningCalling trap with wrong arguments, e.g., Reflect.get(target, key, receiver) where receiver is a different proxy
Concrete fix directions. Pick the one that matches your root cause.
- buildEnsure every trap returns the result of Reflect.<trap>(...args) unless you intentionally override behavior
- buildWhen overriding, maintain the expected return type: boolean for set/deleteProperty, value for get, etc.
- buildAvoid using the proxy inside its own trap; use the target object instead
- buildIf you need to use a proxy as receiver, pass the proxy explicitly as the third argument to Reflect.get
- buildWrap proxy creation in a try-catch to catch TypeError for revoked proxies
- buildUse Proxy.revocable only when you explicitly need to revoke; otherwise use new Proxy(target, handler)
A fix you cannot prove is a guess. Close the loop.
- verifiedWrite a unit test that performs property get/set/has/delete on the proxy and asserts correct behavior
- verifiedLog the return value of each trap and compare with expected Reflect return
- verifiedUse Object.getOwnPropertyDescriptor to confirm property attributes are preserved
- verifiedCheck that the proxy is not revoked by calling console.log(proxy) – revoked proxies throw on any operation
- verifiedRun the code in strict mode to catch silent failures like returning undefined from set
- verifiedProfile the call stack to ensure no infinite recursion is occurring
Things that make this bug worse or harder to find.
- warningDon't forget the return statement in trap functions – it's the #1 cause of silent failures
- warningDon't use the proxy as the receiver in Reflect calls unless you intentionally want to propagate traps
- warningDon't assume traps are optional – if you define a trap, it replaces the default behavior entirely
- warningDon't call Proxy.revocable if you never intend to revoke; use new Proxy to avoid accidental revocation
- warningDon't ignore the 'this' context in trap functions – arrow functions may capture wrong this
- warningDon't rely on console.log alone – stepping through with debugger is better for understanding flow
Silent 'set' Trap Causes Application Data Corruption
Timeline
- 09:15User reports that form data is not saving; state remains unchanged.
- 09:30I inspect the Redux store and see that dispatch actions are processed but state doesn't update.
- 09:45I notice that the reducer uses a proxy-wrapped state object to enforce immutability.
- 10:00I add console.log inside the proxy 'set' trap and see it fires but returns undefined.
- 10:05I recall that 'set' must return a boolean; returning undefined is falsy, so the operation fails.
- 10:10I fix the trap to return Reflect.set(...) and the form saves correctly.
- 10:15I deploy the fix and verify with automated tests.
I was debugging a mysterious bug where form data changes were not persisting in the application state. The UI would update optimistically, but a page refresh would revert all changes. The Redux DevTools showed that the action was dispatched and the reducer ran, but the state object did not update. I suspected the proxy we used for immutable state updates.
Our team had implemented a proxy-based immutable state wrapper to detect mutations. The proxy had a 'set' trap that was supposed to freeze the target after each set. I added a console.log in the trap and saw that it logged the set operation, but the return value was undefined. I realized that the trap didn't have a return statement, so it returned undefined, which coerces to false, causing the proxy to reject the set.
The fix was simple: add 'return Reflect.set(...)' at the end of the trap. I also added a unit test that asserts the set trap returns true. The lesson: always consult the ECMAScript specification for trap return types. A trap that doesn't return the correct type can silently break proxy behavior.
Root cause
The 'set' trap lacked a return statement, returning undefined instead of the required boolean, causing the proxy to reject the set operation.
The fix
Added 'return Reflect.set(target, property, value, receiver);' to the 'set' trap.
The lesson
Always return the correct type from proxy traps. For 'set' and 'deleteProperty', the return value must be a boolean. Omitting a return or returning undefined will make the operation fail silently.
Each proxy trap has specific return type requirements defined in the ECMAScript specification. For example, the 'get' trap can return any value, but if the property is a non-configurable, non-writable data property, the returned value must match the target's value. The 'set' trap must return a boolean: true if the set succeeded, false if it should fail (in strict mode, returning false throws a TypeError). The 'has' trap returns a boolean, 'deleteProperty' returns a boolean, and 'ownKeys' must return an array.
The most common mistake is forgetting to return a value from a trap function. In JavaScript, a function without an explicit return returns undefined. For 'set', undefined is falsy, so the operation appears to succeed but the target is not updated. For 'get', returning undefined might be acceptable if the property doesn't exist, but if the property exists on the target, the proxy will return undefined instead of the actual value, causing silent data loss.
The 'get' trap receives three arguments: target, property, and receiver. The receiver is the object that will be used as 'this' when the property is a getter. If you pass the proxy itself as the receiver to Reflect.get, it can trigger the proxy's own traps, leading to infinite recursion. For example, if you have a proxy that intercepts 'get' and inside that trap you call Reflect.get(target, key, proxy), it will call the 'get' trap again, causing a stack overflow.
To avoid this, always pass the target as the receiver unless you intentionally want to forward traps. A common pattern is to use a separate 'revocable' proxy for the receiver, but that's rarely needed. If you must use a proxy as receiver, ensure there is a base case, such as checking if the receiver is the target itself.
Proxy.revocable creates a proxy that can be revoked. Once revoked, any operation on the proxy throws a TypeError. This is useful for temporary access, but if you accidentally revoke early, the proxy becomes unusable. Symptoms include 'Cannot perform 'get' on a proxy that has been revoked' errors or, if the error is caught, silent failures if the code swallows exceptions.
Debugging: Check if the proxy was created with Proxy.revocable and if revoke() was called. You can inspect the proxy by trying to access any property in a try-catch. Also, ensure that the proxy is not stored in a closure that later gets revoked. Use new Proxy() for permanent proxies to avoid accidental revocation.
Proxy traps behave differently in strict mode vs non-strict mode. For example, the 'set' trap in strict mode must return true for the operation to succeed; returning false throws a TypeError. In non-strict mode, returning false silently fails (the set is not performed, but no error is thrown). Similarly, the 'get' trap in strict mode may throw if the property is a non-configurable, non-writable data property and the returned value doesn't match.
Always test your proxy code in strict mode to catch these issues early. Use 'use strict' at the top of your module or in your test files. Many modern frameworks (like React) run in strict mode, so a proxy that works in development may fail in production.
Frequently asked questions
Why does my proxy's 'get' trap return undefined even though the property exists?
Most likely your 'get' trap doesn't call Reflect.get(target, property) or doesn't return its result. If you return a custom value, the proxy will return that instead of the target's value. Also, if the property is a getter on the target and you don't pass the correct receiver, the getter might not execute properly.
What causes 'Maximum call stack size exceeded' when using a proxy?
This is usually caused by infinite recursion: a trap calls the proxy itself instead of the target. For example, in a 'get' trap, if you do 'return proxy[property]' instead of 'return Reflect.get(target, property)', it will call the 'get' trap again, leading to a stack overflow. Always use Reflect methods with the target as the first argument.
How do I check if a proxy has been revoked?
You can wrap any proxy operation in a try-catch. If the proxy is revoked, it throws a TypeError. Alternatively, you can keep a flag from Proxy.revocable() – the revoke function can set a boolean. But the best way is to avoid revocable proxies unless necessary.
Why does my 'set' trap work in development but fail in production?
The most common reason is strict mode differences. Development often runs in non-strict mode, where returning false from 'set' fails silently. Production might use strict mode (e.g., ES modules are always strict), causing a TypeError when 'set' returns false. Ensure your 'set' trap returns the result of Reflect.set(...) which returns the correct boolean.
Can I use a proxy as the receiver in Reflect.get?
Yes, but you must be careful to avoid infinite recursion. If you pass the proxy as the receiver, the getter on the target might see the proxy as 'this', which can be useful for intercepting accesses. However, if the getter itself tries to access a property on the proxy, it will trigger the 'get' trap again. To avoid this, ensure that the getter does not rely on proxy behavior, or use a separate internal object.