Asher Cohen
Back to posts

Weird JavaScript: Surprising Behaviors and Quirks

Exploring JavaScript's most surprising behaviors — push returns length, floating-point quirks, and other unexpected language features

Introduction

JavaScript has its share of surprising behaviors that can trip up even experienced developers. Understanding these quirks helps you avoid bugs and appreciate the language's unique characteristics.

Push Returns Length

One of the most common surprises: Array.push() returns the new length, not the array:

// ❌ Surprising — push returns length, not the array
const arr = [1, 2, 3];
const result = arr.push(4);
console.log(result); // 4 (the new length!)
console.log(arr);    // [1, 2, 3, 4]

// ❌ Common mistake — chaining
const arr2 = [1, 2];
// arr2.push(3).push(4); // TypeError: arr2.push(3) returns 3, not an array

// ✅ Correct — push then continue
arr2.push(3);
arr2.push(4);

Why? push() follows the convention of mutating methods returning the operation result, not the object. Similar to how delete returns a boolean.

Numbers Are All Floating-Point

JavaScript has no true integer type — all numbers are IEEE 754 doubles:

// Surprising precision issues
console.log(0.1 + 0.2);        // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false

// Integer limits
console.log(9007199254740992 === 9007199254740993); // true!
// Beyond 2^53, integers lose precision

// Division quirks
console.log(0.5 / 0);  // Infinity
console.log(-0.5 / 0); // -Infinity
console.log(0 / 0);    // NaN

typeof null === 'object'

A famous historical bug that can never be fixed:

console.log(typeof null); // 'object' — should be 'null'

// ✅ Correct null check
if (value === null) { /* ... */ }

// ✅ Check for objects properly
if (typeof value === 'object' && value !== null) { /* ... */ }

NaN Is Not Equal to Itself

console.log(NaN === NaN); // false

// ✅ Use Number.isNaN()
console.log(Number.isNaN(NaN)); // true

// ❌ Global isNaN coerces
console.log(isNaN('hello')); // true (misleading!)

Array.sort() Is In-Place and Lexicographic by Default

// ❌ Surprising — sorts as strings by default
console.log([1, 2, 10, 20].sort()); // [1, 10, 2, 20]

// ✅ Always provide compare function
console.log([1, 2, 10, 20].sort((a, b) => a - b)); // [1, 2, 10, 20]

// ❌ sort() mutates the original array
const arr = [3, 1, 2];
arr.sort();
console.log(arr); // [1, 2, 3] — original mutated!

parseInt() and Radix

// ❌ parseInt without radix can surprise
console.log(parseInt('08')); // 8 (was 0 in old engines!)
console.log(parseInt('0x10')); // 16 (hex!)

// ✅ Always specify radix
console.log(parseInt('08', 10)); // 8
console.log(parseInt('0x10', 16)); // 16

The + Operator Overload

// + is both addition and concatenation
console.log(1 + 2);      // 3
console.log('1' + '2');  // '12'
console.log(1 + '2');    // '12' (number coerced to string!)
console.log('1' + 2);    // '12'

// ❌ Unexpected
console.log([] + []);    // '' (empty string!)
console.log([] + {});    // '[object Object]'
console.log({} + []);    // 0 (in some contexts) or '[object Object]'

this Binding Surprises

const obj = {
  name: 'Alice',
  greet: function() {
    console.log(`Hello, ${this.name}`);
  }
};

// ✅ Works
obj.greet(); // 'Hello, Alice'

// ❌ Loses this
const fn = obj.greet;
fn(); // 'Hello, undefined'

// ❌ Arrow functions don't have their own this
const obj2 = {
  name: 'Bob',
  greet: () => console.log(`Hello, ${this.name}`)
};
obj2.greet(); // 'Hello, undefined' (this is from outer scope)

Array Length Is Writable

const arr = [1, 2, 3, 4, 5];
arr.length = 3;
console.log(arr); // [1, 2, 3] — truncated!

arr.length = 5;
console.log(arr); // [1, 2, 3, empty × 2] — extended with holes!

Comparison Quirks

// Loose equality surprises
console.log(null == undefined); // true
console.log(null === undefined); // false

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

// Object comparison
console.log({} == {});  // false (different references)
console.log([] == []);  // false

// Relational comparison with different types
console.log('2' > 1);   // true ('2' converted to 2)
console.log('2' < '12'); // false (string comparison!)

Best Practices to Avoid Surprises

Do's

// ✅ Use === instead of ==
if (value === null) { /* ... */ }

// ✅ Always provide compare function to sort()
[1, 10, 2].sort((a, b) => a - b);

// ✅ Always specify radix with parseInt()
parseInt('08', 10);

// ✅ Use Number.isNaN() instead of isNaN()
Number.isNaN(value);

// ✅ Use toSorted() for immutable sort (ES2023)
const sorted = arr.toSorted();

Don'ts

// ❌ Don't chain after push()
arr.push(1).push(2); // TypeError

// ❌ Don't use == for comparisons
if (value == null) { /* ... */ } // Use ===

// ❌ Don't sort without compare function
[1, 10, 2].sort(); // Wrong order

// ❌ Don't use parseInt without radix
parseInt('08'); // Ambiguous

// ❌ Don't rely on this in detached functions
const fn = obj.method; fn(); // this is lost

Conclusion

JavaScript's quirks are a product of its history and design philosophy:

  • push() returns length — Know your return values
  • Floating-point math — Use epsilon comparisons, work in cents
  • typeof null — Always check === null explicitly
  • NaN !== NaN — Use Number.isNaN()
  • sort() is in-place — Use toSorted() for immutability

These aren't bugs — they're features you need to understand. Master them and you'll write more robust JavaScript.

#javascript #quirks #weird-js #debugging #web-development