Debug session: Javascript, toString, object, maps e outras porcarias
Fun debugging session that happened at $WORK.
I found that a component, published to npm was crashingwhen giving a certain input.
Since it was minified, it was hard to see where the error was. I added the sourcemap. It happened in a loop, a certain property which was expected to be an array, ended up being a Number
:
|
|
Firefox error message was kinda confusing:
|
|
Chrome’s was better:
|
|
That’s the thing with TypeScript: types are not real.
Anyway, after some debugging I found that it happened when the string toString
is used. I handcrafted a minimum reproducible example, which helped me debug. Debuggers are cool, but Conditional Breakpoints often let me down.
To give some context, the code iterates a tree and merges nodes’ values with the same name. It’s a feature for sandwich viewing if you are interested.
What I found curious is that when I logged the node, it told me it was a function with properties!
|
|
This is something I kinda forgot about, but why wouldn’t be possible?
|
|
Back in the day we used to write constructors using functions:
|
|
And of course, ES6 classes have some syntax sugar over this implementation (but not only that!).
Anyway, what was happening is that we created a map-like object. And then to not initialize twice, we did a check for the presence of an existing item:
|
|
However, since toString
always exist in an object, it wasn’t being initialized correctly!
Then it would spiral in some crazy madness I won’t go deep about.
The solution was to just replace the map-like object for a real Map:
A Map does not contain any keys by default. It only contains what is explicitly put into it.
An Object has a prototype, so it contains default keys that could collide with your own keys if you’re not careful.
I didn’t explain where the properties of toString
were coming form, to figure out I used a handy Proxy:
|
|
Whenever toString
is set
(ie overridden), the debugger
is called.
With that I managed to find the offending code:
|
|
So if name
is for example, toString
, hash[name]
returns the function from the prototype chain, ie from Object.prototype.toString
, which attributes are added, leaking to every single object.