Everything is Broken and I Don't Know Why, Part 2 of Infinity
There are few things in life that we enjoy more than good, healthy, broken code. It’s inevitable that things are going to break, it’s inevitable that we’re going to need to debug those things, and it’s inevitable that we’ll need do whatever is necessary to fix them. No one ever ships 100% perfect code.
In this series we delve into broken code and all the many ways we can do a better job fixing it. Why do we spend five hours fixing a bug, when we can spend five minutes fixing it instead? Today we’re continuing on with the topic we started in our first post: browser JavaScript errors. If you haven’t already, take a look at that first post.
I just wrote some really bad code. I did it on purpose ( though I also occasionally write bad code on accident). It looks like this:
// awesome.js
(function() {
var things = [
{foo: 'bar'}
];
function showThing(index) {
console.log(things[index].food);
}
showThing(1);
})();
What exactly do we have here?
We can see we have a function showThing
. This takes an arbitrary index as an argument. We expect this index to exist in this arbitrary things
array and we’re calling the foo
property on it. As you can likely already tell, if we call the showThing
then the result of thing’s index is going to be undefined and undefined is not going to have a property foo
.
If we fire up node.js to execute this, we can see that this does, as prophesied above, throw a TypeError
. And to go with that error, we can also access this very useful stack trace.
/stuff/awesome.js:9
console.log(things[index].foo);
^
TypeError: Cannot read property 'foo' of undefined
at showThing (/stuff/awesome.js:9:30)
at /stuff/awesome.js:12:3
at Object.<anonymous>(/stuff/awesome.js:14:2)
We see that there is a TypeError: Cannot read property
blah blah blah, but then right below that we can see that this occurred within the showThing
function. With that showThing
function it also shows us exactly the line number where it happened and which column number raised the exception. If we keep traversing down the stack we can see that console.log(things[index].food);
was exactly where showThing
was called. It’s an anonymous function, so it doesn’t have a name in the call stack.
This is great! We can pretty easily solve the problem now. Except things don’t always look this way. Just to note these differences between what various browsers show, let’s look at what one random version of Firefox does:
TypeError: things[index] is undefined
showThing@http://localhost:8000/awesome.js:9
@http://localhost:8000/awesome.js:12
@http://localhost:8000/awesome.js:2
It’s the same concept in that you still have the stack property, but since it’s a string it’s completely different.
This changes from browser version to browser version too. What Firefox 25 does may be completely different from Firefox Quantum. Same for Chrome. Same for Safari. There’s simply no standard around how these stack traces are formatted even within the same browser family. Which means if we were to write something that extracted all of them from different browsers, we’d need a ton of crazy regular expressions to be able to capture all of these different cases, as well as always be feature detecting to tell us which ones will give us which sort of stack trace.
And then how about this? What if we — and we definitely do — minify our code? Then we end up with something like:
$ uglifyjs awesome.js -m
(function(){var o=[{foo:"bar"}];function n(n)
{console.log(o[n].foo)}n(1)})();
We’ve removed all the whitespace and shortened our symbol names. If we look closely, there’s a function n, which signifies what showThing
was previously.
If we run this code again we get the exact same exception. Except now our stack trace says the problem is on line 1, column 125:
/stuff/awesome.min.js:1
o=[{foo:"bar"}];function n(n){console.log(o[n].foo)}n(1)
TypeError: Cannot read property 'foo' of undefined
at n (/stuff/awesome.min.js:1:125)
at /stuff/awesome.min.js:1:131
at Object.<anonymous> (/stuff/awesome.min.js:1:137)
This makes the problem a bit more complicated to debug. In our example it’s still more-or-less deductible, but then we only have basically one function in play and also wrote it bad on purpose. If this was a real application, it might be line 1, column 20,000.
Also, we see in the stack that it happened at function n(n)
. What is “n” exactly? Thanks to minification, there can now be multiple n’s, leaving us with no idea what’s actually happening here. We’re essentially clueless at this point.
Thankfully, though, we have source maps, which have been around for a while now. Here’s the why-on-earth-is-this-in-a-Google-Doc-? proposal for version 3 of source maps from way back in 2011.
Ultimately, source maps are JSON files that contain information on how to map your transpiled source code back to their original source. If you’ve ever done programming in a compiled language like Objective-C, you can think of source maps as JavaScript’s version of debug symbols. If you haven’t done programming in a compiled language like Objective-C, then just be happy about that and continue reading.
What this means for our example is that if we look at min.js, we get an exception on line 1, col 125. What the source map will tell us is that we’re actually looking at line 9, column 30 of the unminified JavaScript. It will also translate that symbol “n” to the specific showThing
function. Now we’re making progress. We can apply a source map to our minified source and get back the code we would have had if we weren’t running minified source.
There’s a caveat, though: source maps require three things to actually be useful.
The filename. Obviously. That’s the script it’s coming from.
The line number.
The column number.
Without all three pieces of this information, the source map is literally useless since we can’t take real action with it. It can also even be figuratively useless if you have a particularly colorful metaphor to which you’d like to apply it.
error.prototype.stack
just sucks as a string. We need to keep track of all these regular expressions, we need to keep track of browser evolution, and there’s quite simply no standard around this. So we can’t just come up with one regular expression that solves the problem for everybody. We need all this feature detection, we have to know which version of each browser to parse that specific exception out, we just have a mess.
Well, what can we do then? Find out in our next edition of Everything is Broken and I Don’t Know Why. And be sure to read our first edition while you’re at it. Even if you already read it once.
Also, it’s worth pointing out that if you use Sentry to track exceptions in your client-side JavaScript applications that we automatically fetch and apply source maps to stack traces generated by errors. This means that you’ll see your original source code — and not minified and/or transpiled code, without needing to worry about any of the above.