Home / Articles / Ten Recurring JavaScript Habits That Quietly Undermine Your Codebase

This article is published in English.

Ten Recurring JavaScript Habits That Quietly Undermine Your Codebase

Explains ten common JavaScript and TypeScript pitfalls, from loose equality to state mutation, and shows safer patterns to replace each one.

1748 words

Nearly every JavaScript project you pick up, no matter the company or the framework of the moment, tends to carry the same small set of recurring problems. They're rarely exotic bugs or unpredictable edge cases. They're the same handful of habits, showing up again and again.

None of these issues will break your application immediately. That's precisely why they're so risky. They lie dormant until the project scales up, a new team member starts touching the code, or usage suddenly spikes, and only then do they surface as the kind of bug that swallows an entire afternoon. Below are the ten patterns that show up most often, along with better alternatives.

1. Using == Instead of ===

The loose equality operator in JavaScript coerces types before comparing them, and the outcomes are notoriously hard to predict:

0 == "0"        // true
0 == ""         // true
"" == "0"       // false
null == undefined // true

Sure, there's an internal logic to these coercion rules once you've memorized them. But nobody should have to keep that mental model loaded just to write a simple conditional. Default to === everywhere. It checks both value and type at once, giving you exactly the comparison you intended, with no hidden conversions.

if (userInput === "0") { ... }  // clear, predictable

2. Mutating State Directly

This mistake produces bugs that are especially painful to track down, because the symptom often shows up far away from the actual cause:

function addItem(cart, item) {
  cart.items.push(item); // mutates the original array
  return cart;
}

If that cart object is being observed somewhere else, inside React state, a Redux store, or any system that detects change by comparing references, this kind of in-place mutation is completely invisible to it. The reference itself never changes, so no re-render fires, no subscriber is notified, and you end up chasing a UI that mysteriously refuses to update.

function addItem(cart, item) {
  return { ...cart, items: [...cart.items, item] };
}

Producing a brand-new object or array instead of modifying the original one does use a bit more memory. In exchange, you get predictable state changes, which is a trade worth making far more often than it seems.

3. Not Handling Promise Rejections

An async function that throws without being wrapped in try/catch, or a .then() chain missing a .catch(), tends to fail quietly. In the browser it might do nothing visible at all; in Node it can produce an unhandled rejection warning that's easy to miss among the rest of your logs.

async function getUser(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}
getUser(42); // if this fails, where does the error go?
async function getUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`Request failed: ${res.status}`);
    return await res.json();
  } catch (err) {
    logger.error("Failed to fetch user", { id, err });
    throw err;
  }
}

Any async function capable of failing needs an explicit strategy for that failure case. Leaving it unhandled isn't really a strategy, it's just a bug waiting to go off later.

4. Deeply Nested Callbacks

Nobody sets out to write callback hell on purpose. It builds up gradually, one additional asynchronous step at a time, until the code is nested six levels deep and the indentation resembles a staircase:

getUser(id, (user) => {
  getOrders(user.id, (orders) => {
    getShipping(orders[0].id, (shipping) => {
      updateUI(shipping); // and it keeps going
    });
  });
});

async/await was introduced specifically to undo this kind of nesting:

async function loadShippingInfo(id) {
  const user = await getUser(id);
  const orders = await getOrders(user.id);
  const shipping = await getShipping(orders[0].id);
  updateUI(shipping);
}

The underlying asynchronous behavior is identical, but now the logic reads top to bottom, roughly the way you'd narrate it out loud.

5. Global Variables Sneaking In

Skip a const, let, or var declaration outside strict mode, and JavaScript will silently bind that variable to the global object rather than raising an error:

function calculateTotal() {
  total = 0; // no declaration — this is now global
  for (const item of items) total += item.price;
  return total;
}

That total variable now lives outside the function scope, free to collide with any other variable of the same name elsewhere in the codebase, either overwriting something else or getting overwritten itself depending on execution order. Adding "use strict" at the top of a file converts this into an immediate, visible error instead of a silent, delayed one. Modern module syntax using import/export applies strict mode automatically, so this class of mistake becomes much rarer once you're working with ES modules.

6. Comparing Objects and Arrays With ===

This mistake often shows up in people who took rule #1 a bit too much to heart. === checks objects and arrays by reference, not by what's inside them:

{ a: 1 } === { a: 1 }       // false
[1, 2, 3] === [1, 2, 3]     // false

Two objects that look identical are still separate entities in memory, so strict equality treats them as unequal. To compare actual content, you need a deep-comparison approach: a helper like lodash's isEqual, JSON.stringify for straightforward cases, or a custom comparison routine. Using === here isn't a syntax error, it's simply answering a different question than the one you actually wanted answered.

7. Not Cleaning Up Event Listeners and Timers

Every call to addEventListener, setInterval, or a subscription is a promise that something will eventually clean it up. Neglect that promise and you introduce a memory leak, one that's easy to miss during development but expensive once it's running in production:

useEffect(() => {
  window.addEventListener("resize", handleResize);
  // no cleanup — this listener never goes away
}, []);
useEffect(() => {
  window.addEventListener("resize", handleResize);
  return () => window.removeEventListener("resize", handleResize);
}, []);

8. Overusing any in TypeScript

any isn't really a type so much as an exit ramp, and leaning on it too often turns a strongly typed codebase back into an untyped one, quietly, without anyone actually choosing that outcome:

function processPayment(data: any) {
  return data.amount * data.rate; // no safety net at all
}

Every time you touch data here, you're guessing. The compiler has no way to flag a typo, a missing property, or a mismatched type, because you've explicitly told it to stop checking. Even a loosely defined type beats no type at all:

type PaymentData = { amount: number; rate: number };

function processPayment(data: PaymentData) {
  return data.amount * data.rate;
}

If you genuinely don't know the shape of something yet, unknown is the honest counterpart to any. It requires you to narrow the type before you can use it, rather than letting you act on an unverified assumption.

9. Ignoring the Difference Between null and undefined

JavaScript gives you two separate ways to express "nothing here," and codebases that use them inconsistently end up littered with checks like this:

if (value === null || value === undefined) { ... }

That pattern is usually a sign nobody ever agreed on a convention. A tidier approach is to assign each value a distinct meaning: undefined means "this was never set," and null means "this was deliberately set to nothing." From there, the nullish coalescing operator lets you test for both at once, without writing the comparison twice:

const displayName = user.nickname ?? "Anonymous";

?? only kicks in when the left-hand value is null or undefined. That's different from ||, which also falls back for 0, "", or false, values that are frequently legitimate and shouldn't be treated as missing.

10. Writing Code for the Computer Instead of the Next Person

The final item on this list isn't a syntax error, yet it does more cumulative damage than the other nine put together. A dense one-liner can feel gratifying to write and costly for anyone else to read:

const r = a.filter(x=>x.a).map(x=>x.b).reduce((a,b)=>a+b,0);

It works. But whoever reads it afterward, including you a few months from now, has to reverse-engineer what a, x, and the rest of the chain actually stand for before making any safe changes.

const activeUserBalances = users
  .filter((user) => user.isActive)
  .map((user) => user.balance);

const totalActiveBalance = activeUserBalances.reduce((sum, balance) => sum + balance, 0);

This version takes a couple more lines, but it's instantly clear without any decoding required. JavaScript tends to reward cleverness in the moment and charge for it later, and "later" is almost always someone other than whoever originally wrote the code.

The Pattern Underneath All Ten

Look past the specifics and none of these ten items are really about obscure JavaScript trivia. They're all about predictability: comparisons that behave the way they read, state that doesn't shift behind your back, errors that get caught instead of vanishing silently, and code whose structure matches what it actually does. Address these ten habits, and what remains is ordinary debugging, the kind that comes with every codebase, rather than the self-inflicted kind that quietly consumes your afternoon.