JS errors and why concatenation is bad for devs

Right and wrong directions

In this article I want to take on a journey through bad code and show you how to defened yourself from such. Or, more importantly: I want you to know where the problem lies. I'm going to tell you how browsers execute code and why it matters. If you don't have time for full details, you might just want to skip to the end.

The broken webpage

So you are a frontend dev or a JS dev or maybe a webmaster... Whatever you call yourself, I assume you do some stuff in JS. You code your stuff, paste snippets, download libraries, you refresh the page and everything works. But then comes a day when something needs to be changed. You remove this little thing, refresh and everything breakes. But why?

Somewhere out there, there is some code, code that never has any owner 😉, some code like this:

// init state
//...
document.querySelector('#this_surely_exists').style.display='none'

And now nothing works. Even basic JS interactions might be broken. From the title you know why – concatenation is bad for devs. You've merged all scripts and this one line has broken everything, no libraries works, no carousels are spinning... Or is it the case? Is anything still working? Usually something does work and it might be even more annoying to figure out why one thing does work, while the other one does not. Especially when code is only broken after concatenation and obfuscation.

Scope of failures

How browsers are executing code and why it matters? It matters because when your code fails badly, the rest of it will not be executed. Let that sink in for a moment...

Yes, all the rest will not be executed. No, functions do not isolate failures. Let's do some code examples. Note that for each code example below, you could put that code in script tags. You can also copy&paste each of this in JS console (DevTools). This will let you see how it works in your browser.

Function fail test

console.log('this jsfailtests works');
 
// timeout will be (should be) executed even after next lines fail
setTimeout(function() {
    console.log('this setTimeout works?');
    a = new doesnot_exist_a();
    console.log('Does this still work?');
}, 1000);
 
// first line of this function will work
(function() {
    console.log('this function works');
    a = new doesnot_exist_a();
    console.log('Does this still work?');  // no, it won't work
})();
 
// 1st line _would_ work if not for fail in the above function
console.log('Does this still work?');      // nope, we are dead by now
a = new doesnot_exist_b();
console.log('Does this still work?');
 
// you won't wee any alerts
alert('This will never run.');

So, the function above is defined and executed in one go. It's called an immediately invoked function expression. It is sometimes used to isolate variables. But it won't isolate failures.

Let's imagine you have a separate script tag. The script tag above failed, but would the one below work? Yes. It would be executed. Script tags are isolated from each other (in terms of failures, at least).

// 1st line should work (even thought the previous script tag failed)
console.log('this jsfailtests_isolation works');
a = new doesnot_exist_2b();
console.log('Does this still work?');
 
// you won't see any alerts
alert('This will never run.');

There is another thing going on up there – a timeout was defined before the function was executed. Will it work? Yes, logs would show "this setTimeout works". The timeout was defined before the line that failed.

Function at the end test...

So is the definition order crucial to what is executed? No.

console.log('this jsfailtests_fnlater works');
 
// this will work even though it is defined after code that fails
fnlater();
 
// 1st line _would_ work if it weren't for the fail in the above function
console.log('Does this still work?');  // nope, fnlater has already failed
fnlater_glob_c = new doesnot_exist_fnlater_glob_c();
console.log('Does this still work?');
 
var fnlater_var_d = 1;
 
// you won't see any alerts
alert('This will never run.');
 
// first line will work
function fnlater() {
    fnlater_fun_a = 1;
    console.log('Does this fnlater work?');
    fnlater_fun_b = new doesnot_exist_fnlater();
    console.log('Does this still work?');
}

So the function fnlater worked. Weird, right? Even though it is defined after some code that fails...

Even more interesting things are true (you can run above code in your JS console and then check values of each of this):

  1. fnlater function – will be defined and available to other script tags as a function (even though it is defined after code that fails).
  2. fnlater_fun_a variable, assigned in a function – will be defined and equal to 1.
  3. fnlater_fun_b variable, also assigned in a function – will not be defined at all.
  4. fnlater_glob_c variable – will not be defined.
  5. fnlater_var_d variable – will be defined, but with an undefined value. Wait, what? Why?...

So what the hell?!... Let me explain:

  1. fnlater_fun_b, fnlater_fun_a are both defined in a function fnlater. They should be visible outside of the function because they are not defined with var, let, nor const. The difference is that fnlater_fun_a got assigned and assignement for fnlater_fun_b failed.
  2. fnlater_glob_c is not defined because it is assigned after the function that failed.
  3. So what is it with fnlater_var_d? And why is fnlater defined? Shouldn't fnlater_glob_c = new doesnot_exist_fnlater_glob_c block execution of the function definition?

Well, it works because of hoisting. Variables defined with var and function definitions are hoisted up to the top of their scope. So effectively they are defined on top of the script tag (in this case). And so fnlater_var_d and fnlater were hoisted up to the top of their scope. And they are also both in a global scope.

You can check it by adding another script tag:

console.log({fnlater});  // this will show a function
try {
    console.log({fnlater_fun_b});   // this will throw an exception, because the variable is not defined at all
} catch (e) {
    console.error('See, this global is not available at all', e);
}
try {
    console.log({fnlater_glob_c});  // this will throw too
} catch (e) {
    console.error('See, this global is not available at all', e);
}
console.log('global variable defined with var:', {fnlater_var_d});
console.log('global variable defined in a function:', {fnlater_fun_a});

Function definition vs assignement

That hoisting part might be a bit confusing for functions. Function definition is like a var variable but not exactly the same. The definition like function fun(){} is not exactly the same as var fun = function (){}. Not in terms of hoisting.

Let's consider this example:

console.log('this jsfailtests_fnlater_var works');
 
// this will NOT work even though the function is defined below
fnlater_var();
 
// you won't see any alerts
alert('This will never run.');
 
// first line will work
var fnlater_var = function () {
    fnlater_fun_a = 1;
    console.log('this fnlater_var works?');
    fnlater_fun_b = new doesnot_exist_fnlater();
    console.log('Does this still work?');
}
console.log('this jsfailtests_fnlater_var2 works');
// this will work (it will show undefined though)
console.log({fnlater_var})

So this is mostly equivalent (in terms of effects after execution):

function fnlater() {}
var fnlater = function () {}

Both define a fnlater variable of type function. In both cases fnlater definition is hoisted to top.
But... fnlater = function is still an assigment. And assigment is not hoisted to top.

So that is why this will not work:

fnlater_var();
var fnlater_var = function () {}

But console.log(fnlater_var) will still work when run in JS console.

So that was fun. One can only hope never to throw up with error on the top level 😉. Your code doesn't like it and it will give you a headake.

Epilog: ES6 classes

And if your are not dizzy yet, you might be after this. Functions used to be used to define classes (prototype and all that). So classes act the same, right? Nope, sorry.

Remeber that I said functions are like var variables? Classes are not. This are fully equivalent:

class MindFck {}
let MindFck = class {}

Yep. Class is a let variable. Hence you cannot redefine a class (you have to re-assign it).
See more here: How to redefine JavaScript (NOT CSS) classes, in the console? Stackoverflow.

But why?! Why is class different? Well, this is actually easy. let variables are not hoisted up. I would guess ECMAScript commitee (TC39) figured they didn't want classes to be hoisted up. Hoisting is infamous for causing confusion. If you didn't know why hoisting is confusing I think you know now.

Summary

So to sum up:

  • FF, Chrome, IE all isolate script tags from each other. Fail in one script doesn't brake others.
  • Any fail, also in an immediately executed function, is halting JS execution (within one script tag).
  • So effectively any lines that are after the failed line are not executed.
  • Code from timeout is executed (as long as setTimeout was defined before code failure). Similarly for events.
  • Hoisting makes order of things weird.

If you concatenate all scripts, you risk losing all of them. Isolating widget scripts (standalone components) will help you to reduce some problems. In reality though, you should simply know that the scope of failures in JS is far beyond just one line or function.

My advice:

  • Separate vendor scripts from your extensions. So that at least basic interactions always work.
  • Try to make your code robust. E.g. check if an element actually exists when you query it.
  • Try-catch around fragile code... But remeber to report problems. There is nothing worse than empty console when a bug occured.
  • Do feature detection for new functions. Disable components and functions which cannot be used in old browsers. Note: Detect if a function exists, don't detect browsers.
  • If you want to use new JS syntax wait at least 2 to 5 years before it settles or compile (transpile) scripts... Or keep extensions/components with new syntax in a separate file and load it as a separate module. Note! Try-catch around new syntax might not be enough!