My website had a silly bug in production for a year.
It was quite inconsequential (I think), and I only found it when I (finally) added Sentry error monitoring to the stack (yay!). But it got me thinking about some of the things we’re starting to take for granted in the web framework world — and how we might get tripped up as a result.
SyntaxError: Identifier 'button' has already been declared
My website is built with Eleventy. I’m using the Eleventy JavaScript template language and this is mainly because I moved over from Next.js and wanted to retain much of the React-based component patterns I was used to at the time.
In my header component, which is an HTML file added as an “include” on my base layout using Liquid syntax, there’s a block of inline JavaScript that powers the dark and light theme toggle, selecting the toggle button and assigning it to the variable name button.
const button = document.querySelector("[data-theme-toggle]");
On my blog index page, I import a separate JavaScript file that powers search functionality powered by Algolia. In this file, I manually create the “reset search” button for the search form, and assign it to the variable name — yes, you guessed it — button
.
const button = document.createElement("button");
On the blog index page is where this error triggered, given that both button variables were declared with the same name. But what’s interesting is that I didn’t see this error in the browser console in development because the variables were assigned in different scopes: the theme toggle button was in global scope, and the clear search button was in function scope. (Get the scoop on scopes on MDN.)
<!-- theme toggle button defined in global scope in header.html -->
<script>
const button = document.querySelector("[data-theme-toggle]");
</script>
// reset search button defined in function scope in app_search.js
const renderSearchBox = (renderOptions, isFirstRender) => {
// ...
if (isFirstRender) {
// ...
const button = document.createElement("button");
button.textContent = "Clear";
// ...
}
}
Given that I saw no errors in the browser console in development or production pertaining to this issue, and given that the button variables were scoped differently, I inspected the issue further in Sentry to discover that what was actually picked up was a bit of a weird issue in development whilst the Eleventy dev server was reloading some changes, as demonstrated by the stack trace.
But this still got me thinking. Whilst this definitely supports the “don’t use global scope” in JavaScript argument just in case you run into real issues like this, I think it highlights a different issue.
Framework patterns obfuscate the fundamentals
As developers using modern web frameworks, we’ve become accustomed to our JavaScript being scoped and encapsulated by default to the file in which we’re working. Maybe I’m just speaking for myself, but after years of working with libraries such as React, and meta-frameworks that use libraries such as React, I didn’t stop to consider at this point that the inline JavaScript I was writing in my small header component, in global scope, could have an impact on any other JavaScript included anywhere else.
In 2022, I wrote a blog post where I detailed my journey of refactoring my blog from Next.js to Eleventy, and I said this:
It took a little while to shift my thinking from Next.js to Eleventy. Next.js has a justifiably opinionated way of architecting a front end application — and that's fine — but in moving to Eleventy, it showed me I had perhaps become too reliant on the patterns of Next.js. In going back to web basics with Eleventy, and focussing on shipping plain HTML, CSS and JavaScript to the browser, I feel like I've refreshed and reinvigorated my knowledge of how the web works natively.
So I did learn the thing. But I forgot to remember the thing. If you’ve read this far, why not go back in time and see what you forgot to remember you learned? I need to do it more often.
Top comments (4)
Some of the concepts of Javascript are quite -- strange. ES6 Modules are kind of isolated, but the global context is still visible inside a module. This is very different to modules or libraries in other languages where you need to import any dependency manually. So, in general, not using the global context is a good rule of thumb.
Did you know that ALL Identifiers are also visible in the global context? And the implementation breaks all the rules JS might ever have. This works:
but it is very prone to errors:
Ooof yes that's wild!
That's a challenge I don't really understand: with all the modern tooling, linting, and code assistance, there are still so many antipattern that should be obvious to detect, but instead the tools often warn about irrelevant details or point into the wrong direction, even distracting us from the actual problems.
Red squiggly lines in VSCode everywhere where there aren't any errors, distracting us from the real problems 🫠