Javascript, the good, bad and ugly.

Contents
<p>Brendan Eich wrote the first version of JavaScript in ten days in May 1995, under pressure to ship something for the Netscape Navigator 2.0 beta. He had wanted to put Scheme in the browser; management wanted something that looked like Java, because Java was the hot thing that year. So he took the functional core of Scheme, the prototype-based objects of Self, and bolted on a curly-brace syntax that resembled Java without behaving anything like it. The result was called Mocha, then LiveScript, and finally — in a marketing deal with Sun — JavaScript. Three names and a deadline measured in days. Three decades later that ten-day prototype runs in every browser on earth, on servers, on embedded boards, and increasingly inside my home automation. I have shipped a lot of JavaScript and I have sworn at a lot of JavaScript, often in the same afternoon. This is the honest tour: where it earns its place, where it irritates, and where it genuinely gets ugly.</p> <h2 id="why-the-language-is-the-way-it-is">Why the language is the way it is</h2><div class="ad-unit ad-in-article" aria-label="Advertisement"> <span class="ad-label">Advertisement</span> <ins class="adsbygoogle" style="display:block;text-align:center" data-ad-client="ca-pub-3726833845844946" data-ad-slot="3291553914" data-ad-format="auto" data-full-width-responsive="true"></ins> <script>(adsbygoogle = window.adsbygoogle || []).push({});</script> </div> <p>Almost every complaint people have about JavaScript traces back to that origin story. The ten-day deadline meant rough edges shipped. The &ldquo;must look like Java&rdquo; mandate gave it a syntax that promises class-based, statically-typed behaviour and then delivers prototype-based, dynamically-typed behaviour underneath — a mismatch that confuses people for years. And the fact that it landed in the browser, where breaking old pages is unforgivable, means the language can almost never remove a mistake. Bad decisions become permanent. Understanding that history is the single most useful thing you can do before judging the language, because it explains why the good and the ugly live so close together.</p> <p>The other thing worth knowing up front: there is the <em>language</em> (ECMAScript, governed by TC39, with a yearly spec since ES2015) and there is the <em>ecosystem</em> (npm, bundlers, frameworks, the whole churning mess). Most of the day-to-day pain is the ecosystem, not the language. I&rsquo;ll keep them separate where I can, because conflating them is how people end up shouting at the wrong thing.</p> <h2 id="the-good">The good</h2> <p><strong>It is genuinely everywhere, and that is a real superpower.</strong> One language runs the browser, the server (Node, Deno, Bun), build tooling, serverless functions, desktop apps via Electron, and a surprising amount of IoT. I can write a validation function once and run the identical code on the client for instant feedback and on the server where it actually matters for security. No other language has that reach without a compile-to-target hop. For a small team or a solo tinkerer, the cognitive saving is enormous.</p> <p><strong>The asynchronous model is excellent.</strong> This is the part people underrate. JavaScript&rsquo;s single-threaded event loop with non-blocking I/O is a fantastic fit for the thing most software actually does: wait on the network and the disk. <code>async</code>/<code>await</code> (standardised in ES2017) turned callback hell into code that reads top-to-bottom:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="c1">// Fetch in parallel, fail loudly, and don&#39;t block the event loop </span></span></span><span class="line"><span class="cl"><span class="kr">async</span> <span class="kd">function</span> <span class="nx">loadDashboard</span><span class="p">(</span><span class="nx">userId</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="p">[</span><span class="nx">profile</span><span class="p">,</span> <span class="nx">orders</span><span class="p">]</span> <span class="o">=</span> <span class="kr">await</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">([</span> </span></span><span class="line"><span class="cl"> <span class="nx">fetch</span><span class="p">(</span><span class="sb">`/api/users/</span><span class="si">${</span><span class="nx">userId</span><span class="si">}</span><span class="sb">`</span><span class="p">).</span><span class="nx">then</span><span class="p">((</span><span class="nx">r</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="nx">r</span><span class="p">.</span><span class="nx">json</span><span class="p">()),</span> </span></span><span class="line"><span class="cl"> <span class="nx">fetch</span><span class="p">(</span><span class="sb">`/api/users/</span><span class="si">${</span><span class="nx">userId</span><span class="si">}</span><span class="sb">/orders`</span><span class="p">).</span><span class="nx">then</span><span class="p">((</span><span class="nx">r</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="nx">r</span><span class="p">.</span><span class="nx">json</span><span class="p">()),</span> </span></span><span class="line"><span class="cl"> <span class="p">]);</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">{</span> <span class="nx">profile</span><span class="p">,</span> <span class="nx">orders</span> <span class="p">};</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s2">&#34;dashboard load failed:&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="k">throw</span> <span class="nx">err</span><span class="p">;</span> <span class="c1">// let the caller decide what to render </span></span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></td></tr></table> </div> </div><p><code>Promise.all</code> fires both requests at once instead of one-after-the-other, and the whole thing reads like synchronous code while never blocking. When I want a service to handle thousands of mostly-idle connections, this model is hard to beat and harder to get wrong than threads-and-locks.</p> <p><strong>Modern JavaScript is a different language from the one people remember.</strong> Since ES2015 the additions have been steady and genuinely good: <code>let</code>/<code>const</code> instead of <code>var</code>, arrow functions, destructuring, modules (<code>import</code>/<code>export</code>), optional chaining (<code>obj?.a?.b</code>), nullish coalescing (<code>x ?? fallback</code>), template literals. Most of the &ldquo;wat&rdquo; demos that get passed around mock idioms no sensible person writes any more.</p> <p><strong>The ecosystem, for all its sins, is vast and fast-moving.</strong> Whatever you need, someone has built it, and tooling like Vite or esbuild has made builds quick rather than glacial. When I want automated dependency hygiene to keep that ecosystem from rotting, I lean on the same pattern I describe in <a href="/story/renovate-bot-automated-dependency-updates-that-dont-break-everything/">Renovate Bot: automated dependency updates that don&rsquo;t break everything</a> — npm makes the dependencies easy; keeping them current is the discipline you add yourself.</p> <p><strong>You can read it incrementally.</strong> Because the language is dynamic and the browser console is always one keypress away, JavaScript has the shortest possible feedback loop of any mainstream language. Open dev tools, paste an expression, see the result. For learning, prototyping, and the kind of &ldquo;what does this API actually return&rdquo; spelunking I do constantly, that immediacy is worth a great deal. No compile step, no boilerplate <code>main()</code>, no project scaffolding — just type and run. The flip side, of course, is that the same property lets you ship broken code just as fast, which is the recurring theme of this whole article.</p> <h2 id="the-runtime-landscape-briefly">The runtime landscape, briefly</h2><div class="ad-unit ad-in-article" aria-label="Advertisement"> <span class="ad-label">Advertisement</span> <ins class="adsbygoogle" style="display:block;text-align:center" data-ad-client="ca-pub-3726833845844946" data-ad-slot="3291553914" data-ad-format="auto" data-full-width-responsive="true"></ins> <script>(adsbygoogle = window.adsbygoogle || []).push({});</script> </div> <p>It used to be that &ldquo;JavaScript on the server&rdquo; meant Node.js, full stop. That&rsquo;s no longer true, and it matters because the runtime you pick shapes the pain you get. Node is the boring, ubiquitous default — enormous ecosystem, runs everywhere, and the one I still reach for when I want zero surprises in production. Deno (from Node&rsquo;s original creator) tried to fix Node&rsquo;s mistakes: secure by default, TypeScript out of the box, a sane standard library, no <code>node_modules</code> black hole. Bun went the other direction and optimised for raw speed and a batteries-included toolchain — runtime, bundler, test runner and package manager in one binary. I&rsquo;ve moved scripting and CI glue onto Bun for the speed, kept production services on Node for the boredom, and that split is a perfectly defensible 2020s answer. The lesson is that &ldquo;JavaScript is slow&rdquo; or &ldquo;JavaScript has no standard library&rdquo; are complaints about <em>a</em> runtime, not <em>the</em> language, and you can often fix them by swapping the floor under your code rather than rewriting it.</p> <h2 id="the-bad">The bad</h2> <p><strong>Dynamic typing scales badly.</strong> In a 50-line script, no types is liberating. In a 50,000-line codebase, it is a slow bleed of runtime errors that a compiler would have caught at your desk. <code>undefined is not a function</code> is the national anthem of JavaScript debugging. The community&rsquo;s answer is TypeScript, and it is a good answer — but the fact that the standard fix for the language is a <em>different language layered on top</em> tells you something honest about the base.</p> <p><strong><code>npm install</code> pulls the world.</strong> A modest project routinely drags in hundreds of transitive dependencies, any one of which can be abandoned, compromised, or simply broken by a careless minor release. The <code>left-pad</code> incident in 2016 — where un-publishing an eleven-line package broke builds across the internet — was the moment everyone realised how deep the tower goes. Supply-chain risk here is not theoretical; it is Tuesday.</p> <p><strong>The tooling churns.</strong> The framework and bundler landscape reinvents itself often enough that &ldquo;JavaScript fatigue&rdquo; became a recognised phrase. Pick boring, well-supported tools and ignore most of the noise; chasing every new framework is a hobby, not a strategy.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span><span class="lnt">7 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="c1">// The single most common &#34;bad&#34; bug: silent type coercion </span></span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="mi">0</span> <span class="o">==</span> <span class="s2">&#34;&#34;</span><span class="p">);</span> <span class="c1">// true — loose equality coerces </span></span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="mi">0</span> <span class="o">===</span> <span class="s2">&#34;&#34;</span><span class="p">);</span> <span class="c1">// false — strict equality does not </span></span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">([]</span> <span class="o">+</span> <span class="p">[]);</span> <span class="c1">// &#34;&#34; — arrays stringify to empty </span></span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">([]</span> <span class="o">+</span> <span class="p">{});</span> <span class="c1">// &#34;[object Object]&#34; </span></span></span><span class="line"><span class="cl"><span class="c1">// The fix is a one-line habit: always use === and !==, </span></span></span><span class="line"><span class="cl"><span class="c1">// and lint for == with eslint&#39;s eqeqeq rule. </span></span></span></code></pre></td></tr></table> </div> </div><h2 id="the-ugly">The ugly</h2> <p><strong><code>this</code> is a trap.</strong> What <code>this</code> refers to depends on <em>how a function is called</em>, not where it&rsquo;s defined, and that single design choice has wasted more developer-hours than any other feature. Arrow functions (which inherit <code>this</code> lexically) defused most of it, but the footgun is still loaded for anyone who forgets.</p> <p><strong>Type coercion is genuinely indefensible.</strong> <code>==</code> performs a baroque set of conversions that produce results no one wants. The fix is trivial — always use <code>===</code> — but the default being wrong is the kind of permanent scar the browser&rsquo;s backward-compatibility promise leaves behind. The same instinct that makes me distrust silent coercion is why I distrust silent failures in shell scripts too, which I get into in <a href="/story/bash-the-good-bad-and-ugly/">Bash, the good, bad and ugly</a>: a language that hides its mistakes is a language that ships your mistakes to production.</p> <p><strong>Floating-point money.</strong> <code>0.1 + 0.2 === 0.30000000000000004</code>. This isn&rsquo;t unique to JavaScript — it&rsquo;s IEEE 754 — but JavaScript having only one number type (until <code>BigInt</code>) means people reach for floats to represent currency and lose pennies. Store money as integer cents, always.</p> <p><strong>Backward compatibility means the mistakes are forever.</strong> <code>var</code> hoisting, <code>typeof null === &quot;object&quot;</code>, automatic semicolon insertion that occasionally rewrites your intent — none of it can be removed without breaking pages from 2003. The language can only add, never subtract. So the cruft accumulates, and every new developer has to learn which 20% of the language to avoid.</p> <h2 id="troubleshooting-the-bugs-that-waste-your-afternoon">Troubleshooting: the bugs that waste your afternoon</h2> <p>After years of this, the recurring JavaScript bugs are predictable, which means they&rsquo;re fixable:</p> <ul> <li><strong><code>undefined is not a function</code> / <code>cannot read property of undefined</code>.</strong> Almost always an async value read before it arrived, or a typo in a property name that dynamic typing happily allowed. Reach for optional chaining (<code>obj?.method?.()</code>) and adopt TypeScript on anything that outlives a weekend.</li> <li><strong>A loop that &ldquo;doesn&rsquo;t wait&rdquo;.</strong> <code>forEach</code> does not await async callbacks. Use a plain <code>for...of</code> loop with <code>await</code> inside, or <code>await Promise.all(items.map(...))</code> if order doesn&rsquo;t matter. This one catches everyone exactly once.</li> <li><strong><code>==</code> surprises.</strong> Turn on eslint&rsquo;s <code>eqeqeq</code> rule and the problem stops existing.</li> <li><strong>&ldquo;It works in dev, breaks in prod&rdquo;.</strong> Usually a dependency version drift or a build-tool difference. Lockfiles (<code>package-lock.json</code>) and pinning your Node version solve most of it.</li> <li><strong>Mystery state in callbacks.</strong> Nine times out of ten it&rsquo;s <code>this</code> bound wrong. Switch the method to an arrow function or bind explicitly.</li> </ul> <p>The meta-lesson: lint aggressively, type your code, and pin your dependencies. JavaScript rewards discipline imposed from outside because the language imposes very little from inside.</p> <h2 id="is-it-worth-it-who-is-this-for">Is it worth it? Who is this for?</h2> <p>Yes — with eyes open. If you touch the web at all, JavaScript is not optional, and pretending otherwise wastes energy you could spend taming it instead. For front-end work it is the only game in town. For back-end work it&rsquo;s a legitimately strong choice when your workload is I/O-bound and you value sharing code with the front end; it&rsquo;s a weaker choice for CPU-heavy number crunching, where I&rsquo;d reach for something else.</p> <p>My actual advice, distilled: write <strong>TypeScript</strong>, not raw JavaScript, on anything bigger than a script. Use <code>===</code> always, store money as integers, keep your dependency tree as shallow as you can stomach, and treat the framework hype cycle as background noise. Do that and the language is genuinely pleasant — fast to write, fast to run, runnable everywhere. Skip that discipline and the same ten-day origin story that made JavaScript ubiquitous will happily make your codebase miserable. It&rsquo;s a language that gives you exactly as much rope as you ask for; the trick is knowing how much to ask for.</p>
Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.