Understanding the "Cannot create proxy with a non-object as target or handler" Error

If you‘ve worked with JavaScript Proxies, you may have encountered the "Cannot create proxy with a non-object as target or handler" error. In this article, we‘ll take an in-depth look at what Proxies are, how they work, and what causes this particular error.

As a Linux and networking expert, I‘ll also draw some comparisons between JavaScript Proxies and the proxies we use in system administration. While they operate at different layers of abstraction, there are some interesting conceptual similarities.

What are JavaScript Proxies?

JavaScript Proxies are objects that wrap other objects (called targets) and intercept fundamental operations like property access and function invocation. They allow you to define custom behavior that gets executed before or after the original operation.

The basic syntax for creating a Proxy looks like this:

const proxy = new Proxy(target, handler);

The target is the original object to be wrapped and the handler is an object that defines trap functions for various operations. These trap functions have names like get, set, and apply which correspond to retrieving properties, setting properties, and calling functions respectively.

Here‘s a simple example of using a Proxy to log property accesses:

const target = {
  name: ‘Alice‘,
  age: 30
};

const handler = {
  get: function(target, prop, receiver) {
    console.log(`Property ${prop} was accessed.`);
    return Reflect.get(...arguments);
  }
};

const proxy = new Proxy(target, handler);

proxy.name; // Logs: "Property name was accessed."

In this code, we define a handler object with a get trap. This trap logs a message to the console any time a property is accessed on the proxy. The Reflect.get() call performs the default behavior of returning the value of the property from the original target object.

One thing to note is that the trap functions in the handler object are optional. If a trap is not defined, the default behavior will be used instead. This is what allows Proxies to selectively intercept operations while falling back to the original behavior in other cases.

Networking Proxies vs JavaScript Proxies

If you‘re familiar with Linux system administration, you‘ve likely worked with proxies in a networking context. A forward proxy is a server that sits between clients and servers and forwards client requests to the appropriate backend server. Reverse proxies work in the opposite direction by forwarding client requests from the internet to internal servers.

Proxy Server Diagram
(Image Source: Imperva)

While JavaScript Proxies operate at a completely different layer of abstraction, there are some conceptual similarities:

  • Both types of proxies sit between a source and a target
  • Both can intercept and modify operations
  • Both can implement access control and security policies
  • Both provide a way to add functionality without modifying the original target

The main difference is that JavaScript Proxies work at the language level by intercepting operations on objects rather than network requests. They‘re a form of metaprogramming, which means they allow you to write code that modifies the behavior of other code.

Proxy Use Cases

So what are JavaScript Proxies useful for? One common use case is adding validation to object property assignments. Consider this example:

const validator = {
  set(target, prop, value) {
    if (prop === ‘age‘) {
      if (!Number.isInteger(value)) {
        throw new TypeError(‘Age must be an integer‘);
      }
      if (value < 0) {
        throw new RangeError(‘Age must be positive‘);
      }
    }

    target[prop] = value;
  }
};

const person = new Proxy({}, validator);

person.age = 30; // works
person.age = -5; // throws RangeError
person.age = ‘30‘; // throws TypeError

Here the set trap checks the value being assigned to the age property and throws an error if it‘s not a positive integer. This allows us to enforce data integrity without having to modify the person object directly.

Some other potential use cases for Proxies include:

  • Implementing negative array indices like in Python
  • Defining computed properties based on other properties
  • Proxying requests to a backend API
  • Measuring performance of property access and function calls
  • Implementing security policies like access control or sandboxing

Here‘s an example of using a Proxy to add negative array indexing:

function createNegativeArray(arr) {
  return new Proxy(arr, {
    get(target, prop) {
      if (prop < 0) {
        prop = target.length + Number(prop);
      }
      return target[prop];
    }
  });
}

const arr = createNegativeArray([1, 2, 3, 4, 5]);

console.log(arr[-1]); // Output: 5
console.log(arr[-3]); // Output: 3

In this code, the get trap checks if prop is less than zero. If so, it converts it to a positive index by adding it to the array‘s length. This allows us to use negative indices to access elements from the end of the array.

Performance Considerations

While Proxies are a powerful tool, they‘re not without their costs. Intercepting every property access and function call adds overhead that can impact performance.

Here are a few key things to keep in mind:

  • Proxies are about 10-100 times slower than direct property access
  • The more traps you use, the bigger the performance hit
  • Proxies can cause debugging and stack tracing issues since they obscure the original code
  • Proxies can interfere with JavaScript engine optimizations

That said, the performance impact of Proxies is highly dependent on how they‘re used. In many cases, the flexibility they provide outweighs the costs. Just be sure to profile and test your code to ensure Proxies aren‘t causing bottlenecks.

Under the Hood

So how do Proxies actually work? When you create a Proxy, the JavaScript engine creates a new object with an internal reference to the original target object. The Proxy object also maintains a reference to the handler object.

Proxy Diagram
(Image Source: Medium)

Whenever an operation like property access occurs on the Proxy object, the engine checks the handler to see if a corresponding trap exists. If so, it calls the trap function with the appropriate arguments.

Inside the trap, you can perform any custom logic you want. You can modify the operation, log messages, throw errors, etc. If you want to defer to the original behavior, you can use the corresponding Reflect method as we saw in the earlier examples.

One thing to note is that Proxies can only intercept operations that are explicitly defined in the JavaScript specification. They cannot intercept internal operations or operations that are specific to particular JavaScript engines.

The "Cannot create proxy with a non-object as target or handler" Error

Now that we have a solid understanding of how Proxies work, let‘s circle back to the original error message. As the message suggests, you can only create Proxies for plain JavaScript objects. Attempting to create a Proxy for a primitive value like a number or string will throw an error:

const proxy = new Proxy(‘hello‘, {}); // Throws TypeError

The reason for this restriction is that Proxies fundamentally work by intercepting internal operations on objects. Primitives like numbers and strings don‘t have properties or methods that can be intercepted.

Even some non-primitive types are off-limits for Proxies:

  • Functions
  • Arrays
  • Dates
  • RegExps
  • Other Proxies
  • Primitive wrappers (new Number(42), new String(‘hi‘))

These types have internal slots and exotic behaviors that cannot be faithfully emulated by a Proxy. Allowing Proxies to target these types could lead to inconsistencies and violate assumptions made by the JavaScript engine.

Supported Proxy Targets

The solution in most cases is to simply wrap your value in a plain object before creating a Proxy:

const str = ‘hello‘;

const proxy = new Proxy({ value: str }, {
  get(target, prop) {
    if (prop === ‘length‘) {
      return target.value.length;
    }
    return target[prop];
  }
});

console.log(proxy.length); // Output: 5

In this example, we wrap the string ‘hello‘ in an object before creating a Proxy. The get trap checks for the length property and returns the length of the string. All other properties are accessed directly on the target object.

Proxy Alternatives

While Proxies are a versatile tool, they‘re not always the best solution. In some cases, you may be better off using a different approach:

  • Property getters and setters can intercept individual property accesses
  • Object.defineProperty allows you to define custom properties with get/set behavior
  • Object.observe can track changes to an object (deprecated but still available in some engines)
  • Subclassing or composition can be used to extend or modify behavior

For example, here‘s how you could use a getter to log property accesses:

const target = {
  name: ‘Alice‘,
  get age() {
    console.log(‘Age was accessed‘);
    return 30;
  }
};

target.age; // Logs: "Age was accessed"

And here‘s an example of using composition to add validation:

class Person {
  constructor(age) {
    this._age = age;
  }

  set age(value) {
    if (!Number.isInteger(value)) {
      throw new TypeError(‘Age must be an integer‘);
    }
    if (value < 0) {
      throw new RangeError(‘Age must be positive‘);
    }
    this._age = value;
  }

  get age() {
    return this._age;
  }
}

const person = new Person(30);

person.age = ‘30‘; // Throws TypeError

These approaches are more limited in scope than Proxies but can be simpler and more performant in certain situations.

Conclusion

JavaScript Proxies are a powerful metaprogramming feature that allow you to intercept and customize fundamental operations on objects. They have a wide range of potential use cases including validation, access control, performance monitoring, and extending built-in types.

When creating a Proxy, remember that both the target and handler must be plain objects or you‘ll encounter the "Cannot create proxy with a non-object as target or handler" error. Also be aware of the performance implications of using Proxies and consider alternative approaches when appropriate.

Proxies are a complex but versatile tool to have in your JavaScript toolkit. By understanding how they work and when to use them, you can write more expressive, maintainable, and robust code.

References

Similar Posts