Skip to content
JavaScript Basics: Intercept Object Operations with Proxy

JavaScript Basics: Intercept Object Operations with Proxy

October 22, 2026

A Proxy places handlers around selected object operations. It can observe reads, validate assignments, or provide a temporary view that can later be revoked. Reach for an ordinary function or accessor first when that is enough; proxies are most useful when the behavior needs to apply across operations on an object.

These three independent examples were tested on Linux with Node.js v24.19.0. They use only built-in features, make no network requests, and include assertions. The exact files require Node.js 15.0.0 or later because they import node:assert/strict; the submodule was introduced in Node.js 15. That historical minimum is not a recommendation to use an obsolete release: use a currently supported Node.js version. Only the installed version above was executed for this tutorial.

Prepare a scratch directory

You need Bash, a text editor, Node.js on your PATH, and the mktemp, rm, and rmdir utilities. Keep the same terminal open. Save each named source file as UTF-8 in this directory; .mjs selects an ES module. The guarded commands stop if creating or entering the scratch directory fails.

node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

The tested runtime prints:

v24.19.0

1. Observe reads without replacing their results

Save this as observe-object-reads.mjs:

import assert from 'node:assert/strict';

const reads = [];
const article = { title: 'Setup guide', published: false };
const observed = new Proxy(article, {
  get(target, key, receiver) {
    reads.push(String(key));
    return Reflect.get(target, key, receiver);
  },
});
assert.equal(observed.title, 'Setup guide');
assert.equal(observed.published, false);
assert.equal(observed.missing, undefined);
assert.notEqual(observed, article);
assert.deepEqual(reads, ['title', 'published', 'missing']);
console.log(`Reads: ${reads.join(', ')}`);
console.log(`Original title: ${article.title}`);

Run it:

node observe-object-reads.mjs

Output:

Reads: title, published, missing
Original title: Setup guide

The handler’s get method is a trap for property reads through observed. Reflect.get() forwards the operation to the underlying target while preserving the receiver for any getter. Missing properties still produce undefined. The proxy is a separate object identity, not a modified reference to the original.

Reading article.title directly bypasses the proxy, as the final line demonstrates. Forward to target, not back to observed, or the same trap can recursively call itself. Built-in objects with internal slots, such as Map, may need special handling; this example wraps only a plain object. See the Proxy reference.

2. Validate a particular assignment

Save this as validate-proxy-quantity.mjs:

import assert from 'node:assert/strict';

const line = new Proxy({ quantity: 1 }, {
  set(target, key, value, receiver) {
    if (key === 'quantity' &&
        (!Number.isInteger(value) || value < 0 || value > 100)) {
      throw new RangeError('quantity must be an integer from 0 to 100');
    }
    return Reflect.set(target, key, value, receiver);
  },
});
line.quantity = 0;
assert.equal(line.quantity, 0);
line.quantity = 12;
for (const invalid of [-1, 101, 1.5, '3', NaN]) {
  assert.throws(() => { line.quantity = invalid; }, RangeError);
}
assert.equal(line.quantity, 12);
Object.freeze(line);
assert.throws(() => { line.quantity = 20; }, TypeError);
assert.equal(line.quantity, 12);
console.log(`Quantity: ${line.quantity}`);
console.log('Invalid values and frozen writes rejected');

Run it:

node validate-proxy-quantity.mjs

Output:

Quantity: 12
Invalid values and frozen writes rejected

A set trap reports success with a Boolean. Returning the result of Reflect.set() preserves ordinary write failures. After freezing the object, a valid quantity still cannot be written; the false result becomes a TypeError because ES modules use strict mode.

The range check runs before forwarding, so rejected values leave the quantity unchanged. Zero is intentionally valid, and strings are not silently converted into numbers. This is an assignment guard, not a complete schema or security boundary: defining a property directly uses a different operation, and nested objects are not automatically proxied. The ECMAScript proxy set algorithm also enforces invariants for protected target properties.

3. Revoke a temporary view

Save this as revoke-document-view.mjs:

import assert from 'node:assert/strict';

const document = { title: 'Draft notes' };
const { proxy: view, revoke } = Proxy.revocable(document, {});
const copiedTitle = view.title;
assert.equal(copiedTitle, 'Draft notes');
revoke();
revoke();
assert.throws(() => view.title, TypeError);
assert.throws(() => { view.title = 'Changed'; }, TypeError);
assert.equal(document.title, 'Draft notes');
assert.equal(copiedTitle, 'Draft notes');
console.log('Temporary view revoked');
console.log(`Previously copied title: ${copiedTitle}`);

Run it:

node revoke-document-view.mjs

Output:

Temporary view revoked
Previously copied title: Draft notes

Proxy.revocable() provides a proxy and a revocation function. Once revoked, these property operations throw, and calling revoke() again is harmless. Revocation is useful for ending a temporary API view’s lifetime. It cannot erase a value that a caller already copied or invalidate direct references to the target.

An empty handler normally forwards operations unchanged; revocation ends that forwarding. This example copies only a string. If a property returned a nested object, an earlier reference to that nested object would remain usable too. See the ECMAScript revocable proxy specification.

Clean up

In the same terminal, remove only the named files, return to the original directory, and remove the now-empty scratch directory:

rm -- observe-object-reads.mjs validate-proxy-quantity.mjs revoke-document-view.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on