Skip to content
Back to Blog
JavaScriptWeb DevDeep Dive

JavaScript: The Good, The Bad, and The Weird

2025-11-308 min read

JavaScript: The Good, The Bad, and The Weird

JavaScript powers nearly everything on the modern web. It’s flexible, expressive, and incredibly widespread. Yet, beneath its approachable syntax lies a collection of behaviors that can feel charmingly clever—or deeply confusing.

If you’ve ever wondered why [] + [] becomes an empty string, or why 0.1 + 0.2 !== 0.3, this guide will walk you through JavaScript’s quirkiest features and, more importantly, explain exactly why they behave the way they do.


TL;DR

  • Type Coercion: JavaScript converts types implicitly, sometimes leading to surprises
  • NaN Paradox: NaN is a number, but it's not equal to itself
  • Floating Point Limitations: IEEE 754 makes 0.1 + 0.2 imprecise
  • Hoisting: Declarations move to the top of their scope
  • Closures: Inner functions retain access to outer-scope variables

Type Coercion — JavaScript’s Silent Transformer

JavaScript automatically converts values from one type to another when it thinks it’s helpful. This flexibility is powerful but often the source of frustrating bugs.

The Double Personality of the + Operator

Depending on its operands, + changes its role:

console.log(5 + 10);           // 15 (number addition)
console.log('Hello' + ' World'); // "Hello World" (string concatenation)
console.log(1 + '1');          // "11" (number → string)
console.log(1 - '1');          // 0   (string → number)

Key insight: If either operand is a string, + prefers concatenation. Other operators (-, *, /) convert values to numbers before operating.


When Objects Enter the Equation

Here’s where it gets truly weird:

console.log([] + []); // ""
console.log([] + {}); // "[object Object]"
console.log({} + []); // 0   (in some contexts)
console.log({} + {}); // "[object Object][object Object]" OR NaN

Why this happens:

  1. JavaScript tries valueOf() then toString() on objects
  2. [] becomes ""
  3. {} becomes "[object Object]"
  4. A standalone {} can be interpreted as a block statement, not an object literal

This combination produces results that look nonsensical at first glance—but they’re consistent once you understand the underlying rules.


Equality: Why === Is Your Friend

console.log(0 == false);  // true
console.log('' == 0);     // true

console.log(0 === false); // false
console.log('' === 0);    // false

Rule of thumb: Use === unless you absolutely need the quirks of loose equality.


NaN — The Number That Isn’t a Number

NaN (“Not a Number”) sits in JavaScript’s number type—but behaves unlike any number:

console.log(typeof NaN); // "number"
console.log(NaN === NaN); // false

This is by design, following the IEEE 754 floating-point spec.

How to Check for NaN Properly

const value = 0 / 0;

Number.isNaN(value);  // Best option: strict
isNaN(value);         // Works but coerces first

value === NaN will always be false.


Floating-Point Arithmetic — The Classic 0.1 + 0.2 Problem

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

This isn’t a JavaScript bug—it’s a binary floating-point limitation. Just like 1/3 can't be represented exactly in decimal, 0.1 cannot be represented exactly in binary.

Workarounds

(0.1 + 0.2).toFixed(2);     // "0.30"

function areEqual(a, b) {
  return Math.abs(a - b) < Number.EPSILON;
}
areEqual(0.1 + 0.2, 0.3);   // true

Hoisting — What JavaScript Lifts for You

JavaScript "hoists" declarations to the top of their scope before running your code.

var: Hoisted and Initialized

console.log(myVar); // undefined
var myVar = 5;

Behind the scenes:

var myVar;        // hoisted
console.log(myVar);
myVar = 5;

let and const: Hoisted but Uninitialized (TDZ)

console.log(myLet); // ReferenceError
let myLet = 10;

They exist but cannot be accessed until execution reaches their declaration—this gap is the Temporal Dead Zone.

Function Hoisting

greet();  // Works
function greet() { console.log("Hello!"); }

sayHi();  // TypeError
var sayHi = function() { console.log("Hi!"); };

Function declarations are hoisted entirely; function expressions are not.


Closures — JavaScript’s Hidden Superpower

Closures allow functions to remember variables from their defining scope—even after that scope has closed.

Classic Example

function createCounter() {
  let count = 0;
  return () => ++count;
}

const counter = createCounter();
counter(); // 1
counter(); // 2

Practical Use Case: Private State

function createBankAccount(initialBalance) {
  let balance = initialBalance;

  return {
    deposit(amount) {
      balance += amount;
      return balance;
    },
    withdraw(amount) {
      return amount > balance ? "Insufficient funds" : (balance -= amount);
    },
    getBalance() {
      return balance;
    }
  };
}

const account = createBankAccount(100);
account.deposit(50);  // 150
account.balance;      // undefined

Closures give you encapsulation without classes.


Conclusion — Embrace the Quirks

JavaScript’s unusual behaviors stem from its design philosophy:

  • Backward compatibility: old scripts must still run
  • Flexibility: the language tries to “help” instead of failing
  • Accessibility: easy for beginners, expressive for experts

Understanding why these quirks exist turns them from bugs into tools. Master the rules, and JavaScript becomes far more predictable—and far more powerful.


  • You Don’t Know JS (Yet) – Kyle Simpson
  • MDN Web Docs – The authoritative reference
  • JavaScript.info – Modern deep-dive tutorials
  • WAT Talk – Gary Bernhardt’s hilarious breakdown of JS oddities

Share this article:
Aesthetic Controller
Original Solid