ToolMight LogoToolMight

"Guess the Output" JS Trivia Sprint

Test your JavaScript type coercion, hoisting, and scope knowledge in a 60s sprint.

Streak: 0
Score: 0

Ready for the JS Trivia Sprint?

You have 60 seconds to predict what JavaScript snippets log or evaluate to. A single wrong answer breaks your streak and ends the sprint!

Mastering JavaScript Weirdness, Type Coercion & Quirks

JavaScript is celebrated for its flexibility, but infamous for counter-intuitive evaluation rules resulting from dynamic typing, implicit type coercion, and historical ECMAScript specifications. Understanding these nuances is essential for writing resilient code and acing technical frontend interviews.

1. Implicit type coercion and the ToPrimitive abstract operation

Whenever JavaScript encounters mismatched operand types in binary expressions, the engine invokes internal abstract operations specified by ECMAScript. The most critical is ToPrimitive(input [, PreferredType]):

Addition (+) with Strings: '5' + 3 => "53" (String concatenation takes precedence)

Subtraction (-) forces Numbers: '5' - 3 => 2 (String '5' is coerced to number 5)

Array Addition: [] + [] => "" (Both arrays call [].toString(), producing "" + "")

Array plus Object: [] + {} => "[object Object]"

The Not-Array Paradox: [] == ![] => true (![] becomes false, [] coerces to "" then 0, 0 == 0)

Boolean Numeric Value: true + true => 2 (true is coerced to 1)

If you ever need to inspect or validate complex JSON data structures without unexpected coercion side effects, use our JSON Formatter.

2. The JavaScript event loop: call stack, microtasks & macrotasks

JavaScript is single-threaded, coordinating asynchronous operations using an event loop with two distinct task queues:

  • Microtask Queue: Handles Promise.then(), catch(), finally(), queueMicrotask(), and MutationObserver. Microtasks execute immediately after the current synchronous script runs, before rendering or yielding to macrotasks.
  • Macrotask Queue (Task Queue): Handles setTimeout(), setInterval(), setImmediate(), and user UI events. A new macrotask only executes after all pending microtasks have completely drained.
console.log("1");
setTimeout(() => console.log("2"), 0); // Macrotask
Promise.resolve().then(() => console.log("3")); // Microtask
console.log("4");
// Output sequence: "1", "4", "3", "2"

3. Variable hoisting, lexical scope & the Temporal Dead Zone (TDZ)

During the compile phase before code execution, JavaScript scans for variable and function declarations:

  • var is hoisted to the top of its enclosing function scope and initialized with undefined. Referencing it before its line executes returns undefined without throwing an error.
  • let and const are hoisted to the top of their block scope, but remain uninitialized in the Temporal Dead Zone (TDZ). Accessing them before initialization throws a fatal ReferenceError.
  • Function Declarations vs Expressions: Function declarations (function foo() {}) are fully hoisted along with their function body. Function expressions assigned to variables (const foo = () => {}) remain in the TDZ.

4. Prototypal inheritance and 'this' binding mechanics

In JavaScript, the value of this is determined at invocation time by how a function is called:

Default & Implicit Binding

A standalone function call sets this to the global object (or undefined in strict mode). When invoked as obj.method(), this implicitly binds to obj.

Arrow Functions & Lexical 'this'

Arrow functions do not bind their own this, arguments, or super. They permanently inherit this from the enclosing lexical execution context.

5. IEEE 754 binary floating-point numbers & NaN gotchas

JavaScript numbers are 64-bit binary floating-point values (double precision). Because computers store numbers in base-2 binary rather than base-10 decimal, fractions like 0.1 (1/10) cannot be represented cleanly, producing repeating binary decimals. As a result, 0.1 + 0.2 === 0.30000000000000004, making 0.1 + 0.2 === 0.3 evaluate to false. To safely compare floating-point numbers in production, check if the difference is smaller than Number.EPSILON.

6. Explore more developer practice games & utilities

Recreate modern CSS shapes and gradients under the timer in our CSS Speed Challenge, test your input latency on the Dev Reaction Time Test, or benchmark your coding velocity on our Speed Typing Test.

Frequently asked questions

Why does [] + [] evaluate to an empty string?

Binary + coerces arrays to primitives via the internal ToPrimitive operation. Converting an empty array [] yields an empty string "", resulting in "" + "" = "".

Why does [] == ![] evaluate to true?

The logical NOT operator (!) runs first, turning [] into boolean false. Loose equality (==) coerces boolean false to 0, and the empty array [] to "" then 0, leading to 0 == 0, which is true.

Why is typeof NaN equal to 'number'?

NaN stands for "Not-a-Number", but the IEEE 754 floating-point specification defines it as a numeric value representing an undefined calculation (such as Math.sqrt(-1)).

How does the microtask queue differ from the macrotask queue?

Microtasks (Promise callbacks, queueMicrotask) have strict priority over macrotasks (setTimeout, setInterval). The event loop empties all queued microtasks before proceeding to the next macrotask.

What is the Temporal Dead Zone (TDZ)?

The TDZ is the state between entering a block scope and the line where a let or const variable is declared. Accessing the variable in the TDZ triggers an immediate ReferenceError.

How does 'this' binding differ in arrow functions?

Regular functions bind 'this' dynamically at call time based on the invoking context. Arrow functions do not have their own 'this'; they inherit 'this' statically from their outer lexical scope.