# Error "detail"

**URL:** <https://es.discourse.group/t/error-detail/1857>\
**Category:** 💡 Ideas\
**Tags:** proposal\
**Created:** [December 5, 2023, 5:57pm UTC](https://es.discourse.group/t/error-detail/1857 "2023-12-05T17:57:46Z")\
**Posts on this page:** 1\
**Showing post:** 49

<div class="post-metadata">

**Author:** ![dmchurch](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/dmchurch/32/1962_2.png) [@dmchurch](https://es.discourse.group/u/dmchurch)\
**Post date:** [April 22, 2024, 10:58pm UTC](https://es.discourse.group/t/error-detail/1857/49 "2024-04-22T22:58:51Z")

</div>

> [@bergus](#):
>
> Also it's a pretty horrible pattern to create an entire new `class` constructor and prototype object every time you create an `Error` instance. This is definitely not "nicer to engines".

This is an extremely common misconception, which I'll try to clear up! It factors into what @mmkal was asking about engine performance, and it's also at the heart of my other two recent suggestions, [Proposal: Parser Augmentation Mechanism](https://es.discourse.group/t/proposal-parser-augmentation-mechanism/2008) and [JSON.equals(x, y)](https://es.discourse.group/t/json-equals-x-y/2019). The core of it is this:

**ECMAScript engines are not required to implement ECMA-262 to the letter in order to comply with the specification.**

If you don't understand the meaning behind this, expand the following for a deep-dive into how engines can comply with the spec without _following_ the spec:

> **So what does "ECMA-262 compliance" actually mean?**
>
> I've phrased this somewhat provocatively, but the reason for that is that it's a _really_ important to understand when trying to make changes to the spec. The ECMAScript specification describes a conceptual, _theoretical_ ECMAScript implementation. It uses Abstract Operations (AOs) to describe what this conceptual engine does, and a naïve engine implementation might simply execute everything as described, in order. The engine might have a single `JSObject` class which performs all the requisite operations - searching its internal property descriptor map, delegating lookups to its prototype, etc. It would be perfectly ECMA-262 compliant on account of, it would be a _literal_ realization of the spec into code.
> 
> It would also be _extremely_ slow and would not be able to run the majority of modern websites.
> 
> That's why the spec _doesn't_ mandate exactly what an engine must do at all times. All it mandates is that the _observable_ effects of the engine have to be the same as the _observable_ effects of the conceptual engine. For example, let's say you've got a class definition like this:
> 
> ```javascript
> class ValueHolder {
> #value;
> get value() {return this.#value;}
> set value(v) {this.#value = v;}
> }
> const holder = new ValueHolder();
> 
> ```
> 
> The spec says that, whenever you access `holder.value` in an expression, you call the getter function, which returns the current value of the private variable. And when you set `holder.value` to a new value, the spec says that you have to call the setter function, which will set the private variable to the new value.
> 
> The engine, however, can look at this and see that both accessors are the simple "expose the private variable" type, so as long as it knows that the `value` accessors haven't been redefined, it doesn't have to perform an actual function call (which is expensive for CPUs) and it can instead just read or write the private field directly. Because there's no _observable_ way to tell the difference, in this case, between actually calling the accessors or just directly accessing the private field, the engine is allowed to do whichever is faster.
> 
> That requires the engine to have both (a) a certain amount of freedom of implementation, and (b) enough contextual information to be certain about what the actual outcome will be. Take the following four functions as example, expanding on the above:
> 
> ```javascript
> class ValueHolder {
> #value;
> get value() {return this.#value;}
> set value(v) {this.#value = v;}
> }
> const holder = new ValueHolder();
> 
> function getValue1() {
> return holder.value;
> }
> 
> function getValue2(anyHolder=holder) {
> return anyHolder.value;
> }
> 
> function getValue3(anyHolder=holder) {
> const propName = "value";
> return anyHolder[propName];
> }
> 
> function getValue4(anyHolder=holder, propName="value") {
> return anyHolder[propName];
> }
> 
> ```
> 
> Each of these four functions do the exact same thing when called with no arguments: they retrieve the value stored in the `holder.#value` private field. The conceptual ECMAScript virtual machine would have to take all the same steps: locate the `value` descriptor on the `holder` object, call the get accessor, return the value it returns. You might expect a _tiny_ performance difference between the four functions because some of them have to check for missing arguments and populate them, but you'd expect them each to be in roughly the same ballpark. You wouldn't expect something like this:
> 
> | Test case name | Result |
> | --- | --- |
> | Static object, static property | getValue1 x 31,738 ops/sec ±1.00% (65 runs sampled) |
> | Dynamic object, static property | getValue2 x 2,666 ops/sec ±1.14% (18 runs sampled) |
> | Dynamic object, const property | getValue3 x 2,571 ops/sec ±0.95% (62 runs sampled) |
> | Dynamic object, dynamic property | getValue4 x 2,276 ops/sec ±0.94% (16 runs sampled) |
> 
> These are the results I get from running the test on Firefox on my laptop, but you can [test your own browser](https://www.measurethat.net/Benchmarks/Show/30600/0/class-field-access-speed) to see what results you get. The benchmark has a little bit of extra code to work as a proper test fixture - in particular, each "op" of the "ops/sec" is actually 100,000 calls to the associated function, so `getValue4` is actually called 227,600,000 times per second.
> 
> So what's going on here? Well, the engine can look at `getValue1` and be absolutely certain _exactly_ what piece of data is getting accessed. Since `holder` is a const, the engine knows that it's not ever going to reference a different instantiated object, so the _only_ check it has to make is "has this particular object had its `value` definition changed?" And, assuming it hasn't (which it never does), it can just return the value at one unchanging position in memory.
> 
> On the other hand, the `getValue2` and `getValue3` functions don't _know_ that they'll be called with an instance of the `ValueHolder` class; all they know is that whatever object they get called with, they'll be accessing the `value` property. (In `getValue3`, it's obvious for the engine to see that `propName` can only ever be that one string, so it doesn't have to change its behavior.) In all likelihood, the engine will have internalized the `"value"` string at parse time; in other words, it will calculate the hash of the string, compare it with all the other internalized strings to find if something already has that hash, and then just use the shared object it finds if so. Thus, when it comes time to look up the `"value"` property at function-call-time, it can use the hash it already calculated at parse-time to look it up in the internal property map.
> 
> The `getValue4` function, on the other hand, has _no_ guarantees. The defaults are only defaults, and that means that there's no point in internalizing the `"value"` string early; if no one ever calls the function with an absent `propName` argument, that will have been wasted effort. So, when the function gets called, it has to do _all_ the work, at that point in time, every time.

The TL;DR of this is that you should think about ECMAScript programs the way you think about programs written in, say, C or C++, when compiled by an optimizing compiler. It doesn't have to execute that exact code in that exact order, so long as the outputs are correct.

And, in this case, the engine _doesn't_ have to create a new class constructor and prototype every time it executes that code. Let's look at what information the engine has, after it parses this code:

> [@dmchurch](#):
>
> ```javascript
> throw new class extends Error {
> foo = 5;
> bar = "foo";
> }("Error message")
> 
> ```

The engine now knows the following:

- The thrown value will be an anonymous, nameless subclass of `Error`
- It will have two instance fields, `foo` and `bar`
- Those fields will be initialized to `5` and `"foo"`, respectively
- The `Error` constructor for the thrown object will be called with the value `"Error message"`
- No other instance of this anonymous subclass will ever be created

It doesn't need to create a new class every time the code runs, because the class that gets created has the same functionality every time. In all likelihood, the class will create a "shared class" internal structure that encodes all the above information, allocating two "instance field" slots for the declared fields. When the code gets executed, it can create an instance object with those two slots, pointing to the "shared class" as its implementation.

Now, you may think "but `Object.getPrototypeOf(error)` and `error.constructor` need to return different values!" And you're right, they do... _if_ the code ever calls or accesses these. This is why _observability_ is important. The only way for code to _observe_ the return value of `Object.getPrototypeOf(error)` is for it to _call_ `Object.getPrototypeOf(error)` - and browsers know that, in modern JS code, almost _nobody_ ever calls `getPrototypeOf` on an object or accesses the `constructor` field. Knowing that, they make the assumption that it _won't_ be, and they'll clean up afterwards and do it properly if it turns out that was wrong. This is what's known as "fast path" vs "slow path".

So, the smart thing to do here is just leave the constructor and prototype slots blank when you create the instance (I say "blank" and not "`undefined"` because this isn't happening at the level of JS code; it's more likely to be a `nullptr` if anything) and then, _if and when_ the code calls a method that needs to use the value stored there, _then_ the engine can fill them in.

In contrast, when the parser encounters the following code:

> [@bergus](#):
>
> ```javascript
> throw Object.assign(new Error("Error message"), {
> foo: 5,
> bar: "foo",
> })
> 
> ```

It can only be certain of the following:

- A value will be thrown

That's because it doesn't know, at parse time, whether `Object.assign` will still have the same value as the `Object.assign` method we're familiar with. It might assume that it _probably_ is, but it still has to emit the code that checks that, and it has to emit the code that deals with if it isn't - and, if the cost of generating that optimized fast-path is less than the expected benefit of using it, the the engine probably just won't bother. On the other hand, when the engine has _guarantees_ about how the code will be used, then it doesn't have to bother writing the slow path at all.

---

_[View the full topic](https://es.discourse.group/t/error-detail/1857)._
