Intercepting Object Operations
Introduction: Why Intercept Object Operations?
Welcome to the first lesson of our course on Meta-Programming and Advanced Integration Patterns. In this course, we are going to explore how to write code that treats other code as data. This is a powerful skill that allows you to create more flexible and dynamic applications.
In standard programming, we usually interact with objects directly. We read a property, change a value, or delete a key. Meta-programming changes this by allowing us to step in the middle of these actions. This is called "interception." By intercepting these operations, we can add extra logic without changing the original object or the code that uses it.
In the real world, this is very useful for several reasons. You might want to log every time a piece of data is changed for debugging purposes. You could also use it to validate data, ensuring that a user does not set a price property to a negative number. It is also great for access control, where you can prevent certain parts of your program from reading sensitive information. In this lesson, we will focus on building a logging tool that tracks everything happening to an object.
The Proxy Object: Wrapping A Target
To start intercepting operations, JavaScript provides us with a special object called a Proxy. You can think of a Proxy as a wrapper or a "middleman" for another object. The object being wrapped is called the target. Once an object is wrapped, any interaction with that object must go through the Proxy first.
To create a Proxy, we use the new Proxy(target, handler) syntax. The target is the original object we want to watch. The handler is another object that contains our "traps."
If we don't put any traps in the handler, the Proxy will just pass everything through to the target as if it weren't there.
Intercepting Property Access With Get And Set Traps
The most common things we do with objects are reading and writing values. The get trap intercepts any attempt to read a property. The set trap intercepts any attempt to change a property value.
The get trap receives the target and the prop. The set trap additionally receives the value someone is trying to save. By using these traps, we can see exactly what is being read and what is being changed in real-time.
A quick but important detail: a set trap must return a boolean. Returning true means "the write succeeded," and returning false means "the write was rejected." However, in strict mode — which is always on inside JavaScript modules and any file with "use strict" — returning false from a set trap does not fail quietly. Instead, the assignment expression throws a TypeError. Because all the code in this course runs as strict-mode module code, a rejected assignment will surface as a thrown error. For that reason, when we want to reject a write with a clear, custom message, we prefer to throw new TypeError(...) explicitly, as shown above. We still return true for writes that we want to allow.
Notice that we wrap the property key with String(prop) before logging it. A property key can be a string or a Symbol, and string methods are not available on symbols. Converting with String(prop) keeps our traps safe no matter what kind of key is used.
More Useful Traps: Has And DeleteProperty
Beyond reading and writing, we often check if a property exists or try to remove one. The has trap intercepts the in operator, while the deleteProperty trap intercepts the delete keyword.
These traps give you a high level of control over the lifecycle of an object's data, allowing you to make certain properties "permanent" or invisible. Notice that we guard the prop.startsWith call with a typeof prop === "string" check and convert keys with String(prop) before logging — this keeps the traps robust when a key happens to be a Symbol.
Blocking Property Definitions With DefineProperty
There is one more trap that is closely related to set: the defineProperty trap. While the set trap intercepts normal assignments like obj.x = 1, the defineProperty trap intercepts calls to Object.defineProperty(obj, key, descriptor). This is the lower-level API that defines a property along with its descriptor — settings such as whether the property is writable, enumerable, or configurable.
Intercepting defineProperty is useful when you want to fully lock down an object. A determined caller could try to bypass your set trap by calling Object.defineProperty directly, so to build a truly read-only wrapper you must handle this trap too.
The trap receives the target, the property key, and the descriptor object that describes how the property should be defined. Like set, it must return a boolean indicating whether the definition was allowed. Here we simply return false instead of throwing our own error, and this is deliberate: it shows the strict-mode behavior described earlier — a rejected defineProperty automatically throws a TypeError even when you only return false. We throw explicitly (as in the set example) only when we want to attach a clear, custom message; when a plain rejection is enough, returning false is perfectly fine. You will use this trap in the practice exercises to build an object that cannot be modified in any way.
The Reflect API: Your Default Forwarder
Putting It All Together: A Logging Proxy
Summary And Practice Preparation
In this lesson, we introduced the core concepts of meta-programming using the Proxy and Reflect tools. You learned that a Proxy acts as a middleman that uses traps to intercept actions such as reading, writing, and deleting. You also learned that Reflect is the best way to handle the default behavior inside those traps to keep your code working as expected.
By understanding these patterns, you can now build systems that monitor, protect, or transform data automatically. In the upcoming practice exercises, you will get hands-on experience writing your own traps and using the Reflect API to manage object behavior. Get ready to dive into the code!
