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.
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.
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.
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.
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):
fnlater
function – will be defined and available to other script tags as a function (even though it is defined after code that fails).fnlater_fun_a
variable, assigned in a function – will be defined and equal to 1.fnlater_fun_b
variable, also assigned in a function – will not be defined at all.fnlater_glob_c
variable – will not be defined.fnlater_var_d
variable – will be defined, but with an undefined value. Wait, what? Why?...So what the hell?!... Let me explain:
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.fnlater_glob_c
is not defined because it is assigned after the function that failed.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});
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.
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.
So to sum up:
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: