ES6 has been with us for 2 years. ES2016 and ES2017 are standard practice. ES2018 is just around the corner.
And yet, sometimes you still need JavaScript practices so arcane you've almost forgotten they exist. Such was the case with a production bug we discovered after a performance optimization.
When you get
β Swizec Teller (@Swizec) November 9, 2017
Uncaught TypeError: Cannot read property 'call' of undefined
at __webpack_require__
But only on every 4th page load. Sometimes 3rd. Or 7th. pic.twitter.com/7T7gXFwQmi
Every once in a while, you would refresh our webapp and stare at the loading animation forever. Yes, we have one of those because we're cool.
We worked hard on it, and it looks great. But we don't want you to be stuck staring at it never getting to the page.
We traced the problem down to a JavaScript error during app initialization. Sometimes Webpack would try to execute modules before they were ready.
This shows up as a cryptic error π
Uncaught TypeError: Cannot read property 'call' of undefinedat __webpack_require__
It happens on this line inside manifest.js
π
// Execute the module functionmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
Now manifest.js
is a generated file, so there isn't much you can do to fix it. In fact, you shouldn't have to fix it. Or even consider modifying it. Ever.
Webpack generates this file when you compile your code and uses it to bootstrap your code. It builds a list of modules inside that modules
dictionary and executes them when you run import
or require()
.
Usually, this either always works or never works.
It always works when your code is correct. It never works when you forget to export
your module.
But it's never supposed to get into such a situation that it sometimes works and sometimes doesn't. That is right out.
So I started to dig.
And dig.
And dig some more.
Our bug stymied even grandmaster TheLarkInn.
Wat.
β ππππ Sean Larkin (@TheLarkInn) November 10, 2017
I dug for an hour. Then two. Then five.
I tried everything. I added console logs into manifest.js to track which module exactly was causing a problem. I used Webpack's official analyze tool to inspect our builds and associate moduleId
s with specific code files.
At up to 3 minutes per compile, it was slow going.
I changed this and that and nothing worked.
Then I found this old article Jake Archibald published in 2013. Deep dive into the murky waters of script loading
In it, he explained that scripts dynamically inserted into the DOM were async
by default.
π‘
Why does that matter? Because our performance optimization made our scripts preload
then injected them into the DOM when the files were loaded.
This meant that sometimes our main_code.js
would start executing before our main_code_vendor.js
file was loaded. As a result, core libraries our code depends on weren't loaded yet by the time our code tried to use them.
Ha!
Some background
Let me give you some background.
We use Webpack to split our app into multiple files. Libraries go into an app_vendor.js
file, and our code goes into an app.js
file. Most of our apps also have chunks that Webpack loads asynchronously when they're needed.
So you need to load at least 3 JavaScript files to make any of our apps work
- manifest.js
- app_vendor.js
- app.js
Loading scripts as async
is an old technique to make webapps faster. You write <script src="bla" async></script>
and the browser doesn't wait for JavaScript to load before moving on to rendering the rest of your DOM.
This is great, but it leads to problems. Scripts might execute in random order.
So instead, we used defer
for a long time. This downloads scripts without waiting for them, then executes all of them in order as they were defined.
Wonderful.
But preloading is even better π <link rel="preload" href="bla" onload="loadJsFiles(this.href) />
. With preloading, you're loading scripts without waiting for them, potentially before the user even opens your site, then executing a callback to say "Ok we got the script, now what?"
In our case, the "now what" part would create a script
DOM node and insert it into the page. That makes it execute.
function loadJsFile(file) {// keep track of what's been loadedif (allScriptsLoaded) {loadedScripts.forEach(function () {var script = document.createElement("script");script.src = '<%= "#{js_file_href}" %>';document.body.appendChild(script);});}}
Keep track of all scripts that were loaded, insert them into the DOM, and trigger their execution when all are ready.
The solution
This "Preload as much as possible, insert into DOM when all is ready" works great.
Except when it doesn't.
Sometimes they would execute in the wrong order. Our business code would start executing before our vendor code and discover that the libraries it needs weren't there.
π©
But why? We wait until all scripts are preloaded before inserting them into the page.
Because dynamically inserted scripts are async
by default. You have explicitly disable that with script.async = false
.
And everything works.
2013-era JavaScript strikes again. Still relevant.
Learned something new?
Want to become a high value JavaScript expert?
Here's how it works π
Leave your email and I'll send you an Interactive Modern JavaScript Cheatsheet πright away. After that you'll get thoughtfully written emails every week about React, JavaScript, and your career. Lessons learned over my 20 years in the industry working with companies ranging from tiny startups to Fortune5 behemoths.
Start with an interactive cheatsheet π
Then get thoughtful letters π on mindsets, tactics, and technical skills for your career.
"Man, love your simple writing! Yours is the only email I open from marketers and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. π"
Have a burning question that you think I can answer?Β I don't have all of the answers, but I have some! Hit me up on twitter or book a 30min ama for in-depth help.
Ready to Stop copy pasting D3 examples and create data visualizations of your own? Β Learn how to build scalable dataviz components your whole team can understand with React for Data Visualization
Curious about Serverless and the modern backend? Check out Serverless Handbook, modern backend for the frontend engineer.
Ready to learn how it all fits together and build a modern webapp from scratch? Learn how to launch a webapp and make your first π° on the side with ServerlessReact.Dev
Want to brush up on your modern JavaScript syntax?Β Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you areΒ β€οΈ