JavaScript's Gotchas (WTF JS)

***

JavaScript's Gotchas, also known as "WTF JS," refers to unexpected, confusing, or counterintuitive behaviors in the JavaScript language. These behaviors often arise due to type coercion, implicit conversions, quirks in the language's design, and the nuances of the ECMAScript specification. Understanding these gotchas is crucial for developers to avoid bugs and write more predictable, reliable code.

// Arrays and Equality
[] == ![]; // -> true
// Arrays are truthy, so ![] is false, which coerces to 0. [] coerces to 0, so 0 == 0 is true.
 
true == []; // -> false
true == ![]; // -> false
// true converts to 1 and [] converts to 0. 1 != 0.
 
false == []; // -> true
false == ![]; // -> true
// false and [] both convert to 0. 0 == 0.
 
// Type Coercion Oddities
!!"false" == !!"true"; // -> true
!!"false" === !!"true"; // -> true
// Both strings are truthy, so !! converts them to true.
 
"b" + "a" + +"a" + "a"; // -> 'baNaNa'
// +"a" converts 'a' to NaN, so it becomes "ba" + NaN + "a" which is 'baNaNa'.
 
NaN === NaN; // -> false
// NaN is not equal to anything, including itself.
 
// Object Comparison
Object.is(NaN, NaN); // -> true
NaN === NaN; // -> false
 
Object.is(-0, 0); // -> false
-0 === 0; // -> true
// Object.is and === have different behaviors for NaN and -0.
 
// Fun with Syntax
[1, 2, 3] + [4, 5, 6]; // -> '1,2,34,5,6'
// Arrays are converted to strings and concatenated.
 
let a = [, , ,];
a.length; // -> 3
// Trailing commas create empty slots, affecting array length.
 
[] == ''; // -> true
[] == 0; // -> true
[''] == ''; // -> true
[0] == 0; // -> true
[0] == ''; // -> false
[''] == 0; // -> true
[[[[[[]]]]]] == ''; // -> true
[[[[[[]]]]]] == 0; // -> true
// Abstract equality comparison can produce unexpected results.
 
// Number Coercion and Parsing
parseInt(null, 24); // -> 23
// null is converted to a string and then parsed according to the specified radix.
 
parseInt(0.000001); // -> 0
parseInt(0.0000001); // -> 1
parseInt(1 / 1999999); // -> 5
// Parsing floats can produce unexpected results due to string conversion.
 
0.1 + 0.2; // -> 0.30000000000000004
// Floating-point arithmetic can produce imprecise results.
 
true + true; // -> 2
(true + true) * true - true; // -> 1
// Booleans are coerced to numbers in arithmetic operations.
 
Number(); // -> 0
Number(undefined); // -> NaN
// Number without arguments returns 0, with undefined returns NaN.
 
// Unexpected Typeof and Instanceof
typeof NaN; // -> 'number'
// Despite its name, NaN is of type 'number'.
 
typeof null; // -> 'object'
// Null is considered an object in JavaScript, although it is a primitive value.
 
(1).__proto__.__proto__.__proto__; // -> null
// Primitives are wrapped into their object counterparts.
 
const c = "constructor";
c[c][c]('console.log("WTF?")')(); // -> WTF?
// c accesses the constructor property and calls Function.
 
// Miscellaneous
{} + []; // -> '[object Object]'
[] + {}; // -> '[object Object]'
// The order of operations and type coercion produce different results.
 
[10, 1, 3].sort(); // -> [1, 10, 3]
// Default sorting converts elements to strings, sorting them lexicographically.
 
let f = () => {};
f(); // -> undefined
// Arrow function with empty block returns undefined.
 
let f = function() { return arguments; };
f("a"); // -> { '0': 'a' }
// Regular function captures arguments.
 
let f = () => arguments;
f("a"); // -> ReferenceError: arguments is not defined
// Arrow function does not capture arguments.
 
(() => {
  try {
    return 2;
  } finally {
    return 3;
  }
})(); // -> 3
// finally block overrides the return statement.
 
new class F extends (String, Array) {}(); // -> F(0) []
// Extends clause uses the last argument, so class extends Array.
 
let x, { x: y = 1 } = { x };
y; // -> 1
// Destructuring with default value when x is undefined.
 
[...[..."..."]].length; // -> 3
// Spreading a string spreads its characters into an array.
 
foo: {
  console.log("first");
  break foo;
  console.log("second");
}
// -> 'first'
// Labeled block with break statement.
 
typeof new class { class() {} }(); // -> 'object'
// Keyword can be used as method name.
 
var foo = { n: 1 };
var bar = foo;
foo.x = foo = { n: 2 };
foo.x; // -> undefined
bar; // -> { n: 1, x: { n: 2 } }
// Chain assignment with side effect.
 
var obj = { property: 1 };
var array = ["property"];
obj[array]; // -> 1
// Array key is coerced to string.
 
var nestedArray = [[[[[[[[[["property"]]]]]]]]]];
obj[nestedArray]; // -> 1
// Array key is coerced to string.
 
var map = {};
var x = 1;
var y = 2;
var z = 3;
map[[x, y, z]] = true;
map[[x + 10, y, z]] = true;
map["1,2,3"]; // -> true
map["11,2,3"]; // -> true
// Array keys are coerced to strings.
 
Math.min() > Math.max(); // -> true
Math.min() < Math.max(); // -> false
// Math.min() returns Infinity, Math.max() returns -Infinity.
 
null == 0; // -> false
null > 0; // -> false
null >= 0; // -> true
// Different comparisons yield different results.
 
JSON.stringify("production") === "production"; // -> false
// JSON.stringify adds quotes to the string.
 
1 == true; // -> true
Boolean(1.1); // -> true
1.1 == true; // -> false
// Different comparisons with true and numbers.
 
function a(x) {
  arguments[0] = "hello";
  console.log(x);
}
a(); // -> undefined
a(1); // -> "hello"
// Arguments object affects parameter value.
 
setTimeout(() => console.log("called"), Infinity); // -> "called"
// Infinity timeout executes immediately.
 
setTimeout(123, 100); // -> <timeoutId>
// Non-function callback is converted to string and evaluated.
 
27..toString(); // -> '27'
// Double dot allows to call toString on a number.
 
(() => {
  return {
    b: 10
  };
})(); // -> { b: 10 }
// return and the returned expression must be in the same line.
 

๐Ÿ“š Other resources

  • wtfjs.com โ€” a collection of those very special irregularities, inconsistencies and just plain painfully unintuitive moments for the language of the web.
  • Zeros in JavaScript โ€” a comparison table of ==, ===, + and * in JavaScript.

The original idea for WTFJS belongs to Brian Leroux.