Home / Articles

Common JavaScript and TypeScript Pitfalls That Silently Break Code

JavaScript and TypeScript traps that look correct and still break: NaN comparisons, type coercion, async/await timing and equality checks that lie to you.

Publisher: REACTAPP.TOP9 min read1973 words

This article is published in English.

Sources: 20 JavaScript/TypeScript Mistakes That Look Right… But Break Your App

Comparison and Equality Traps

1. NaN is never equal to anything, not even itself.

[1, NaN, 3].indexOf(NaN); // -1, even though NaN is right there

Per the specification, NaN === NaN evaluates to false, so any mechanism built on strict equality—including indexOf—will fail to locate it even when it's sitting right there in the array. Reach for Number.isNaN(value) instead, or use .includes(), which was built specifically to handle this edge case correctly.

2. typeof null returns "object".

if (typeof value === "object") { value.someMethod(); } // value could be null

This is an old quirk baked into the language, and it catches out anyone who treats typeof as a reliable "is this an object" test. null sails through that check and then blows up the instant you try to call a method on it.

3. Comparing objects via JSON.stringify() is order-sensitive.

JSON.stringify({ a: 1, b: 2 }) === JSON.stringify({ b: 2, a: 1 }); // false

Two objects that represent the same data can serialize into strings with keys in a different order—this happens naturally after a trip through a database or a different part of your codebase. Skip string comparison and use a proper deep-equality utility instead.

4. .sort() without a comparator treats everything as text.

[10, 2, 1].sort(); // [1, 10, 2]

Values get stringified first and then compared character by character, which mangles numeric ordering. Always supply an explicit comparator function for numbers, such as (a, b) => a - b.

Type Coercion

5. The + operator switches to concatenation the moment one operand is a string.

function total(price, quantity) {
  return price + quantity; // "5" + 3 = "53" if price came from a form input
}

Values arriving from query parameters, form fields, or parsed JSON frequently remain strings even though they look like numbers. + won't convert the string into a number for you—instead, it converts the number into a string.

6. Default values in destructuring only apply when the property is undefined, never for null.

function greet({ name = "friend" } = {}) { return `Hi, ${name}`; }
greet({ name: null }); // "Hi, null" — the default never kicks in

Passing null explicitly counts as providing a value, not omitting one, so the fallback default is bypassed entirely. If callers might legitimately send null, you need to check for it separately.

Async Timing

7. Running .filter(Boolean) over an array of promises accomplishes nothing.

const results = items.map((item) => checkAsync(item)); // array of Promise objects
const valid = results.filter(Boolean); // every Promise is truthy, filters nothing

A Promise instance is always truthy no matter what it eventually resolves or rejects to. You need to resolve them first with await Promise.all(results), and only then filter the resulting values.

8. Optional chaining stops evaluation for the whole expression, not just the optional part.

user?.getPermissions().includes("admin"); // if user is null, this is undefined, not an error, but also never calls .includes at all

It's tempting to think only the ?. segment gets skipped, but everything chained after it stops running too. That's usually the desired behavior, though it becomes a subtle bug when a trailing call—like a logging or analytics call—silently never executes.

9. Promise.all throws away all results as soon as a single promise rejects.

const [a, b, c] = await Promise.all([taskA(), taskB(), taskC()]);
// if taskC rejects, you get nothing back from taskA and taskB either, even if they succeeded

This is intentional, all-or-nothing behavior. When partial success is acceptable in your situation, Promise.allSettled returns the outcome of every promise—succeeded or failed—rather than discarding successful results because one task failed.

Object and Array Mutation

10. const locks the binding, not the object it points to.

const config = { retries: 3 };
config.retries = 10; // completely legal, const never protected this

Declaring something with const only stops you from reassigning that variable to a different value. The object it references remains just as mutable as if you'd used let. If you need actual immutability, use Object.freeze()—and keep in mind that only freezes one level deep.

11. .sort() mutates the original array, unlike .map() or .filter().

const original = [3, 1, 2];
const sorted = original.sort();
console.log(original); // [1, 2, 3] — also changed, probably not what was intended

It's easy to assume all array methods return a new array, since most of the common ones do exactly that. .sort() and .reverse() are the exceptions, and mutating an array that another part of your code still holds a reference to leads to the classic "this value changed on its own" bug.

12. for...in iterates over inherited properties too, not just the object's own keys.

Array.prototype.customMethod = function () {};
const arr = [1, 2, 3];
for (const key in arr) { console.log(key); } // includes "customMethod" too

This rarely bites anyone in practice, since extending built-in prototypes is uncommon, but any enumerable property inherited from the prototype chain will show up inside a for...in loop. Using Object.keys() or for...of sidesteps this surprise completely.

Functions and Scope

13. Arrow functions never bind their own this.

const counter = {
  count: 0,
  increment: () => { this.count++; }, // `this` here is NOT the counter object
};

An arrow function grabs this from the lexical scope where it was written, not from the object that ends up calling it. This makes them ideal for callbacks that need to hang on to an outer this, but a poor choice for object or class methods that are supposed to reference their own instance.

14. A switch statement falls through silently when you omit break.

switch (status) {
  case "pending":
    console.log("Order is pending");
  case "shipped": // no break above, this runs too
    console.log("Order has shipped");
}

Skip a single break and every subsequent case executes, whether or not its condition actually matched. Deliberate fallthrough does happen occasionally, but it's rare enough that a missing break almost always signals a bug rather than a design choice.

Dates and Numbers

15. 0.1 + 0.2 is not 0.3.

0.1 + 0.2 === 0.3; // false
0.1 + 0.2; // 0.30000000000000004

This comes from standard floating-point representation, and it affects nearly every language, not something unique to JavaScript. Avoid strict equality on computed decimal values; instead, check whether the difference between two numbers falls under an acceptable tolerance.

16. Bare date strings parse as UTC, while slash-separated dates parse in local time.

new Date("2024-01-01");   // midnight UTC
new Date("01/01/2024");   // midnight local time

Two strings that appear to describe the same day can resolve to different calendar dates depending on the timezone of whoever runs the code. Pick one date format and stick to it consistently, and lean on a well-established date library rather than parsing strings by hand.

TypeScript-Specific Traps

17. Type assertions don't validate anything at runtime.

const user = JSON.parse(rawData) as User; // TypeScript trusts you, nothing is actually checked

Writing as User tells the compiler to trust you completely, and it does exactly that, without performing any check that the parsed value actually has the shape you claimed. Bad or unexpected data keeps flowing through your program labeled as verified, until it eventually causes a failure somewhere downstream.

18. The non-null assertion operator only suppresses warnings, it doesn't change reality.

function getName(user?: User) {
  return user!.name; // TypeScript stops warning you; the runtime null check is still gone
}

Appending ! tells TypeScript you're certain a value is neither null nor undefined. If that assumption is wrong, the compiler simply stops flagging the risk, and the failure surfaces at runtime, precisely where the type system used to catch it.

19. Excess-property checking applies only to object literals passed directly, not to variables holding the same shape.

type Config = { retries: number };
function setup(config: Config) {}

setup({ retries: 3, timeout: 5000 }); // error: 'timeout' does not exist on type Config

const options = { retries: 3, timeout: 5000 };
setup(options); // no error — same excess property, but it slips through

Pass an object literal straight into a function call and TypeScript scrutinizes every property, rejecting anything extra. Assign that same object to a variable first, then pass the variable, and the check disappears entirely. It's a genuine quirk in how the type checker behaves, meaning a typo or a stray extra field can quietly slip past validation depending purely on how the value reached the function.

20. Numeric enums accept arbitrary numbers, not just their declared members.

enum Status { Pending, Shipped, Delivered }
function process(status: Status) {}

process(999); // no compile error, even though 999 isn't a declared Status value

Under the hood, numeric enums in TypeScript are just plain numbers, so the type system will accept any numeric value as valid, regardless of whether it corresponds to a declared member. String enums enforce real restrictions here, so when you need the compiler to actually reject bad input, favor string enums or a union built from string literals.

None of these twenty issues are obscure or exotic. Each one is common enough to turn up in real, reviewed, shipped code, precisely because it resembles perfectly ordinary JavaScript or TypeScript until the particular input, timing, or edge case exposes it. Being aware of this list won't replace the need to test your code carefully. What it gives you is a head start: you end up checking for problems you already know to watch for, rather than stumbling onto them after the fact in production.