10 JavaScript Secrets That Senior Developers Don't Explicitly Teach You
I still remember the night I broke production with one line of JavaScript.
It wasn't some complex algorithm. It wasn't even async code.
It was this:
if (userInput == 0) { // do something }
Everything looked fine. Tests passed. The UI worked.
But in production? Chaos.
Because "0", false, "", and [] all decided to join the party.
That was the moment I realized something uncomfortable: nobody teaches you the real JavaScript. You learn it by getting burned.
After years of writing JavaScript across frontend, backend, and cloud systems, these are the things senior devs know but rarely say out loud. Let's fix that.

1. Double Equals Will Betray You
Yes, you've heard it: "Always use ===."
But nobody explains why it matters in real systems — forms, query params, API payloads, and database fields that arrive as strings.
console.log(0 == false); // true console.log("" == 0); // true console.log([] == false); // true console.log(null == undefined); // true
== doesn't compare values. It coerces types until JavaScript can make a guess. In user-facing code, that guess is often wrong.
The senior move: use === by default. Reach for == only when you explicitly want null/undefined equivalence — and leave a comment explaining why.
// Intentional: treat null and undefined as "missing" if (value == null) { return defaultValue; }
2. "Truthy" Is Not "Valid"
JavaScript treats plenty of values as falsy: 0, "", NaN, null, undefined, and false.
Everything else is truthy — including [], {}, and "0".
const count = 0; if (count) { renderBadge(count); // never runs — 0 is falsy } const name = ""; if (!name) { showError("Name required"); // runs — good } if (items) { // runs even when items is [] — empty array is truthy! }
This is why senior devs avoid if (value) when value might legitimately be 0 or "".
Better patterns:
if (count !== undefined && count !== null) { /* ... */ } if (name.trim().length === 0) { /* ... */ } if (items.length > 0) { /* ... */ }
Explicit beats clever when data comes from humans.
3. typeof null === "object" (Yes, Really)
This is a forty-year-old bug that never got fixed.
typeof null; // "object" typeof []; // "object" typeof {}; // "object"
So typeof alone cannot tell you if something is a plain object, an array, or null.
The senior move: combine checks.
function isPlainObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
Or use Array.isArray(), value === null, and Object.prototype.toString.call(value) when you need precision.
4. + Is Addition and Coercion
The + operator is overloaded. If either side is a string, JavaScript concatenates.
"5" + 1; // "51" "5" - 1; // 4 (subtraction coerces to number) [] + []; // "" [] + {}; // "[object Object]" {} + []; // 0 (in some contexts) or "[object Object]" — yes, it's inconsistent
This shows up constantly with form inputs, URL params, and API responses.
The senior move: parse early, at the boundary.
const quantity = Number(input.value); if (!Number.isFinite(quantity)) { throw new Error("Invalid quantity"); }
Don't let strings sneak into math deep inside your app.
5. Array.sort() Does Not Sort Numbers
Default sort converts elements to strings and compares UTF-16 code units.
[10, 2, 1].sort(); // [1, 10, 2] [1, 2, 10].sort(); // [1, 10, 2] — still wrong for numbers
Fix:
[10, 2, 1].sort((a, b) => a - b); // [1, 2, 10]
Also remember: sort() mutates the original array. Senior devs copy first when immutability matters.
const sorted = [...items].sort((a, b) => a - b);
6. NaN Is the Only Value Not Equal to Itself
NaN === NaN; // false Number.isNaN(NaN); // true isNaN("hello"); // true — coerces first! Number.isNaN("hello"); // false
Use Number.isNaN() for actual NaN checks. Use Number.isFinite() when validating numeric input.
function toNumber(value) { const n = Number(value); return Number.isFinite(n) ? n : null; }
7. Closures + var in Loops = Ghost Bugs
Classic interview question. Real production incident.
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // logs: 3, 3, 3
var is function-scoped. By the time the callbacks run, the loop has finished and i is 3.
Modern fix: use let (block-scoped) or capture the value.
for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // logs: 0, 1, 2
If you still see var in legacy code, this is one reason migrations to let/const actually matter.
8. 0.1 + 0.2 !== 0.3
Floating-point math is approximate. Always has been.
0.1 + 0.2; // 0.30000000000000004 0.1 + 0.2 === 0.3; // false
This breaks billing UIs, dashboards, and anywhere money or percentages appear.
The senior move: store money in integers (cents), use a decimal library, or round at display boundaries — never assume IEEE 754 will behave politely.
const total = Math.round((0.1 + 0.2) * 100) / 100; // 0.3
9. Reference Equality vs Structural Equality
Two objects with identical contents are not equal.
{ a: 1 } === { a: 1 }; // false
Same for arrays:
[1, 2] === [1, 2]; // false
This bites you in React useEffect dependencies, memoization, and state comparisons.
useEffect(() => { fetchData(filters); }, [filters]); // runs every render if filters is a new object each time
Fixes: stabilize references (useMemo), compare primitives, or compare serialized snapshots when appropriate.
For deep equality, reach for a utility — or better, design state so you compare IDs and scalars, not nested object trees.
10. async/await Doesn't Catch What You Think
try/catch around an async function only catches errors inside that function's awaited flow. It does not catch forgotten rejections.
async function broken() { try { fetchUser(); // missing await — rejection floats away } catch (err) { console.error(err); // never runs } }
And at the top level:
Promise.reject("oops"); // unhandled rejection — can crash Node in strict mode
Senior habits:
awaitevery promise you care about- return promises from
asyncfunctions intentionally - use
Promise.allSettledwhen partial failure is acceptable - add a global
unhandledrejectionhandler in servers and serious frontends
async function solid() { try { await fetchUser(); } catch (err) { reportError(err); throw err; // rethrow if callers need to know } }
Bonus: JSON.parse Is Not a Clone Button
JSON.parse(JSON.stringify(obj)) is a popular hack — and a footgun.
It drops undefined, functions, symbols, and Date objects (dates become strings). It breaks on circular references. Map, Set, and BigInt? Gone.
Senior devs clone with intent: structured cloning (structuredClone in modern runtimes), immutable updates, or libraries when the shape is complex.
What I'd Tell Past Me
JavaScript is not "weird" because the language hates you. It's weird because it grew up in the browser, carrying decades of backward compatibility on its back.
The seniors aren't gatekeeping. They just stopped being surprised.
Your job isn't to memorize every quirk. It's to know where the cliffs are — coercion, truthiness, references, floating point, and async boundaries — and write code that survives real input from real users.
Start with ===. Question every if (value). Parse at the edges. Copy before you sort. Await what you care about.
You'll ship fewer Tuesday-night incidents. I promise.