diff --git a/questions/explain-hoisting/en-US.mdx b/questions/explain-hoisting/en-US.mdx
index b1a8ab1..7cc1037 100644
--- a/questions/explain-hoisting/en-US.mdx
+++ b/questions/explain-hoisting/en-US.mdx
@@ -29,9 +29,9 @@ The following behavior summarizes the result of accessing the variables before t
## Hoisting
-Hoisting is a term used to explain the behavior of variable declarations in JavaScript code.
+Hoisting is a term used to explain the behavior of declarations in JavaScript code.
-Variables declared or initialized with the `var` keyword will have their declaration "moved" up to the top of their containing scope during compilation, which we refer to as hoisting.
+Variables declared with the `var` keyword have their declaration "moved" up to the top of their containing scope during compilation, which we refer to as hoisting.
Only the declaration is hoisted; the initialization/assignment (if there is one) will stay where it is. Note that the declaration is not actually moved – the JavaScript engine parses the declarations during compilation and becomes aware of variables and their scopes, but it is easier to understand this behavior by visualizing the declarations as being "hoisted" to the top of their scope.
@@ -39,7 +39,7 @@ Let's explain with a few code samples. Note that the code for these examples sho
### Hoisting of variables declared using `var`
-Hoisting is seen in action here as even though `foo` is declared and initialized after the first `console.log()`, the first `console.log()` prints the value of `foo` as `undefined`.
+Hoisting is visible here: even though `foo` is declared and initialized after the first `console.log()`, the first `console.log()` prints `undefined`.
```js live
console.log(foo); // undefined
@@ -80,7 +80,7 @@ class Foo {
### Hoisting of function expressions
-Function expressions are functions written in the form of variable declarations. Since they are also declared using `var`, only the variable declaration is hoisted.
+A function expression is a function assigned to a variable binding. When the binding uses `var`, only the variable declaration is hoisted — the function body is not.
```js live
console.log(bar); // undefined
@@ -91,6 +91,14 @@ var bar = function () {
};
```
+Arrow functions are function expressions too, so the same rule applies — only the binding is hoisted, and its TDZ behavior follows the declaration keyword (`var` initializes to `undefined`, `let` and `const` remain in the TDZ until their declaration runs).
+
+```js live
+console.log(baz); // undefined
+var baz = () => 'arrow';
+console.log(baz()); // 'arrow'
+```
+
### Hoisting of function declarations
Function declarations use the `function` keyword. Unlike function expressions, function declarations have both the declaration and definition hoisted, thus they can be called even before they are declared.
@@ -104,7 +112,7 @@ function foo() {
}
```
-The same applies to generators (`function*`), async functions (`async function`), and async function generators (`async function*`).
+The same applies to generator functions (`function*`), async functions (`async function`), and async generator functions (`async function*`).
### Hoisting of `import` statements
@@ -128,16 +136,157 @@ However, this statement is [a little different for the `var` keyword](https://tc
> Var variables are created when their containing Environment Record is instantiated and are initialized to `undefined` when created.
+MDN groups hoisting into four observable behaviors, which map to the declaration kinds covered above:
+
+1. **Value hoisting** — the value is usable before the declaration. Applies to function declarations.
+2. **Declaration hoisting** — the binding is usable before the declaration but reads `undefined`. Applies to `var`.
+3. **Scope tainting** — the binding exists from the top of the scope but any access throws (the TDZ). Applies to `let`, `const`, and `class`.
+4. **Side effects** — the declaration's side effects run before the rest of the module evaluates. Applies to `import`.
+
## Modern practices
In practice, modern codebases avoid using `var` and use `let` and `const` exclusively. It is recommended to declare and initialize your variables and import statements at the top of the containing scope/module to eliminate the mental overhead of tracking when a variable can be used.
ESLint is a static code analyzer that can find violations of such cases with the following rules:
-- [`no-use-before-define`](https://eslint.org/docs/latest/rules/no-use-before-define): This rule will warn when it encounters a reference to an identifier that has not yet been declared.
-- [`no-undef`](https://eslint.org/docs/latest/rules/no-undef): This rule will warn when it encounters a reference to an identifier that has not yet been declared.
+- [`no-use-before-define`](https://eslint.org/docs/latest/rules/no-use-before-define): Warns when an identifier is referenced before its declaration appears in source.
+- [`no-undef`](https://eslint.org/docs/latest/rules/no-undef): Warns when an identifier is referenced without being declared anywhere in scope.
+
+## Additional examples
+
+The examples below cover hoisting behaviors that are less obvious from the summary table and that commonly cause confusion.
+
+### Function declaration compared with function expression
+
+```js live
+console.log(declared());
+console.log(expressed());
+
+function declared() {
+ return 'function declaration';
+}
+
+var expressed = function () {
+ return 'function expression';
+};
+```
+
+- `declared()` returns `'function declaration'`. Function declarations are fully hoisted — both the identifier binding and the function body are available from the top of the scope.
+- `expressed()` throws `TypeError: expressed is not a function`. The `var expressed` binding is hoisted and initialized to `undefined`, but the assignment of the function expression happens at its source location. Calling `undefined()` produces the `TypeError`.
+
+### `var` in a `for` loop with `setTimeout`
+
+```js live
+for (var i = 0; i < 3; i++) {
+ setTimeout(() => console.log(i), 0);
+}
+// Output: 3, 3, 3
+```
+
+`var i` is function-scoped rather than block-scoped, so all three callbacks close over the same binding. The loop increments `i` to `3` before any `setTimeout` callback runs, because macrotasks run after the current synchronous code completes. Each callback then reads the current value of the shared `i`, which is `3`.
+
+Two fixes:
+
+- Replace `var` with `let`. `let` is block-scoped, so each iteration creates a fresh binding that the callback closes over.
+- Wrap the body in an IIFE that captures the current value as a parameter: `(i => setTimeout(() => console.log(i), 0))(i)`. This was the pre-ES6 workaround.
+
+### `var` escapes block scope
+
+```js live
+if (true) {
+ var a = 1;
+ let b = 2;
+}
+console.log(a); // 1
+console.log(b); // ReferenceError: b is not defined
+```
+
+`var` is scoped to the nearest function or script, not to the enclosing block. The declaration is hoisted past `if`, `for`, `while`, and plain block statements to the containing function or module scope, which is why `a` is still visible after the `if`. `let` and `const` are block-scoped, so `b` only exists inside the block.
+
+### Redeclaration
+
+```js
+var x = 1;
+var x = 2; // OK — x is now 2
+
+let y = 1;
+let y = 2; // SyntaxError: Identifier 'y' has already been declared
+```
+
+`var` allows the same name to be redeclared in the same scope; the second declaration is a no-op and only the assignment runs. `let`, `const`, and `class` throw `SyntaxError` if the same name is declared twice in the same scope. Like hoisting, this is resolved statically before execution — duplicate lexical declarations are an _early error_ detected during parsing, so no code runs at all.
+
+### Class declarations
+
+```js live
+console.log(typeof Foo);
+
+class Foo {}
+```
+
+This throws `ReferenceError: Cannot access 'Foo' before initialization`.
+
+Class declarations are hoisted — the binding is created at the top of the enclosing block — but they remain in the Temporal Dead Zone until the `class` declaration is evaluated. Any access before that point throws, including `typeof`.
+
+This behavior can be confused with "classes are not hoisted". The two statements are observably different:
+
+- If `Foo` were not hoisted, `typeof Foo` would return `'undefined'` (the behavior for truly undeclared identifiers).
+- Because `Foo` is hoisted but uninitialized, `typeof Foo` throws.
+
+The distinction also matters for `extends` clauses, which are evaluated at class declaration time. `class A extends B {}` throws if `B` is hoisted but still in the TDZ at that point.
+
+### `typeof` and the Temporal Dead Zone
+
+```js live
+console.log(typeof undeclaredVariable); // 'undefined'
+console.log(typeof someLet); // ReferenceError
+let someLet = 1;
+```
+
+`typeof` does not throw when applied to an identifier that has no declaration anywhere in scope — it returns the string `'undefined'`. However, `typeof` does throw when applied to an identifier that is declared but still in the Temporal Dead Zone. The binding exists, and reading it (which `typeof` must do) triggers the TDZ error.
+
+This distinguishes "undeclared" (no binding in any enclosing scope) from "declared but uninitialized" (binding exists, initialization has not yet occurred).
+
+### Shared names across `var` and function declarations
+
+```js live
+function outer() {
+ console.log(inner);
+ inner();
+
+ function inner() {
+ console.log('inner called');
+ }
+
+ var inner = 'overwritten';
+}
+
+outer();
+// Output:
+// [Function: inner]
+// inner called
+```
+
+Two behaviors combine here:
+
+1. Both `var inner` and the `function inner` declaration are hoisted to the top of `outer`.
+2. When a `var` declaration and a function declaration share a name in the same scope, the function declaration takes precedence during initialization. `inner` is initialized with the function object rather than `undefined`.
+
+The `var inner = 'overwritten'` assignment takes effect only after the two `console.log` calls, so those calls observe the function. A `console.log(inner)` after the assignment would print `'overwritten'`.
+
+A `let` or `const` declaration in the same scope as a `var` of the same name produces a `SyntaxError` at parse time, before any code runs.
+
+## Common misconceptions
+
+The following statements appear frequently in explanations of hoisting, including in material generated by large language models, but are incorrect or imprecise:
+
+1. **Classes are not hoisted.** Class declarations are hoisted. They remain in the Temporal Dead Zone until the declaration is evaluated, which is observably different from not being hoisted at all — most notably, `typeof` throws on a class in the TDZ but returns `'undefined'` for a truly undeclared identifier.
+2. **`var` is hoisted; `let` and `const` are not.** All three are hoisted. They differ in initialization: `var` is initialized to `undefined` at hoist time, while `let` and `const` remain uninitialized in the TDZ until their declaration is evaluated.
+3. **`typeof` never throws on undeclared variables.** `typeof` is safe for identifiers that have no declaration anywhere in scope, but it throws in the TDZ. The `typeof x === 'undefined'` guard is only safe if `x` is not declared anywhere in the enclosing scope.
+4. **Function declarations are hoisted; function expressions are not.** Both are hoisted, but only the binding. In `var fn = function () {}`, the `var fn` declaration is hoisted and initialized to `undefined`. In `let fn = function () {}`, the binding is hoisted but remains in the TDZ. The function body is never hoisted for expressions.
## Further reading
- [Hoisting | MDN](https://developer.mozilla.org/en-US/docs/Glossary/Hoisting)
+- [ECMA-262 §14.2.6 Block Runtime Semantics: Evaluation](https://tc39.es/ecma262/#sec-block-runtime-semantics-evaluation)
+- [Temporal Dead Zone | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let#temporal_dead_zone_tdz)
- [What is Hoisting in JavaScript?](https://www.freecodecamp.org/news/what-is-hoisting-in-javascript)
diff --git a/questions/explain-the-difference-between-shallow-copy-and-deep-copy/en-US.mdx b/questions/explain-the-difference-between-shallow-copy-and-deep-copy/en-US.mdx
index aaed330..ea20cc0 100644
--- a/questions/explain-the-difference-between-shallow-copy-and-deep-copy/en-US.mdx
+++ b/questions/explain-the-difference-between-shallow-copy-and-deep-copy/en-US.mdx
@@ -4,22 +4,20 @@ title: Explain the difference between shallow copy and deep copy
## TL;DR
-A shallow copy duplicates the top-level properties of an object, but nested objects are still referenced. A deep copy duplicates all levels of an object, creating entirely new instances of nested objects. For example, using `Object.assign()` creates a shallow copy, while using libraries like `Lodash` or `structuredClone()` in modern JavaScript can create deep copies.
+A shallow copy duplicates the top-level properties of an object, but nested objects are still referenced. A deep copy duplicates all levels of an object, creating entirely new instances of nested objects. `Object.assign()` and the spread operator (`...`) create shallow copies. `structuredClone()` is the modern built-in for deep copies. `JSON.parse(JSON.stringify())` and Lodash's `_.cloneDeep` are other common approaches, each with different tradeoffs around which values they can faithfully clone.
```js live
-// Shallow copy example
+// Shallow copy — nested object is shared
let obj1 = { a: 1, b: { c: 2 } };
-let shallowCopy = Object.assign({}, obj1);
+let shallowCopy = { ...obj1 };
shallowCopy.b.c = 3;
-console.log(shallowCopy.b.c); // Output: 3
-console.log(obj1.b.c); // Output: 3 (original nested object changed too!)
+console.log(obj1.b.c); // 3 — original mutated too
-// Deep copy example
+// Deep copy — fully independent
let obj2 = { a: 1, b: { c: 2 } };
-let deepCopy = JSON.parse(JSON.stringify(obj2));
+let deepCopy = structuredClone(obj2);
deepCopy.b.c = 4;
-console.log(deepCopy.b.c); // Output: 4
-console.log(obj2.b.c); // Output: 2 (original nested object remains unchanged)
+console.log(obj2.b.c); // 2 — original unchanged
```
---
@@ -30,50 +28,207 @@ console.log(obj2.b.c); // Output: 2 (original nested object remains unchanged)
A shallow copy creates a new object and copies the values of the original object's top-level properties into the new object. However, if any of these properties are references to other objects, only the reference is copied, not the actual object. This means that changes to nested objects in the copied object will affect the original object.
-#### Example
-
```js live
let obj1 = { a: 1, b: { c: 2 } };
let shallowCopy = Object.assign({}, obj1);
shallowCopy.b.c = 3;
-console.log(shallowCopy.b.c); // Output: 3
-console.log(obj1.b.c); // Output: 3 (original nested object changed too!)
+console.log(shallowCopy.b.c); // 3
+console.log(obj1.b.c); // 3 — original nested object changed too
```
-In this example, `shallowCopy` is a shallow copy of `obj1`. Changing `shallowCopy.b.c` also changes `obj1.b.c` because `b` is a reference to the same object in both `obj1` and `shallowCopy`.
+`shallowCopy` is a new object, but `shallowCopy.b` and `obj1.b` point to the same nested object, so mutating `shallowCopy.b.c` is visible through `obj1.b.c`.
### Deep copy
-A deep copy creates a new object and recursively copies all properties and nested objects from the original object. This means that the new object is completely independent of the original object, and changes to nested objects in the copied object do not affect the original object.
-
-#### Example
+A deep copy recursively duplicates the original object and every object it references, producing a fully independent tree. Mutations to the clone — including to its nested objects — have no effect on the original.
```js live
-let obj1 = { a: 1, b: { c: 2 } };
-let deepCopy = JSON.parse(JSON.stringify(obj1));
+let obj2 = { a: 1, b: { c: 2 } };
+let deepCopy = structuredClone(obj2);
deepCopy.b.c = 4;
-console.log(deepCopy.b.c); // Output: 4
-console.log(obj1.b.c); // Output: 2 (original nested object remains unchanged)
+console.log(deepCopy.b.c); // 4
+console.log(obj2.b.c); // 2 — original nested object unchanged
+```
+
+`structuredClone` recursively copies `obj2` and its nested `b` object, so `deepCopy.b` is a fresh object independent of `obj2.b`.
+
+## Comparison of common cloning methods
+
+The table below summarizes the behavior of the four most common approaches.
+
+| Value / scenario | `{ ...obj }` / `Object.assign` | `JSON.parse(JSON.stringify())` | `structuredClone()` | Lodash `_.cloneDeep` |
+| --- | --- | --- | --- | --- |
+| Nested objects / arrays | Shared (shallow) | Cloned | Cloned | Cloned |
+| `Date` | Shared reference | Becomes ISO string | Cloned as `Date` | Cloned as `Date` |
+| `undefined` values | Preserved | Dropped | Preserved | Preserved |
+| `NaN`, `Infinity`, `-Infinity` | Preserved | Becomes `null` | Preserved | Preserved |
+| Circular references | Not recursed; references shared | Throws | Supported | Supported |
+| `Map`, `Set` | Shared reference | Becomes `{}` | Cloned | Cloned |
+| `RegExp` | Shared reference | Becomes `{}` | Cloned | Cloned |
+| Functions / methods | Shared reference | Dropped | Throws | Shared reference |
+| `Symbol` keys | Preserved | Dropped | Not cloned | Cloned |
+| Class instances (prototype) | Becomes plain object | Becomes plain object | Becomes plain object | Prototype preserved |
+| `BigInt` | Preserved | Throws on stringify | Preserved | Preserved |
+
+- **Shallow copy** does not recurse into nested objects. For a cyclic input, the shallow copy does not throw, but `copy.self` still points to the original object rather than to the clone, so the cycle is not preserved as a cycle in the new object.
+- **`JSON.parse(JSON.stringify())`** does not preserve `Date`, `undefined`, `NaN`, `Map`, `Set`, `RegExp`, or functions. It throws on circular references and `BigInt`.
+- **`structuredClone()`** clones string-keyed own enumerable data properties, including `Date`, `Map`, `Set`, `RegExp`, typed arrays, `ArrayBuffer`, and cycles. It throws on function and symbol values. Symbol-keyed properties and the prototype chain are not preserved in practice, so the result is a plain object.
+- **DOM nodes** are intentionally omitted from the table. Behavior depends on the specific node type, the host environment, and the tooling involved; a general-purpose cell in a summary table would be misleading.
+
+## Which method should I reach for?
+
+Work through the questions below in order and stop at the first one that applies.
+
+1. **Do only the top-level properties need to be independent?** Use a shallow copy — spread (`{ ...obj }`, `[...arr]`), `Object.assign`, `Array.from`, or `Array.prototype.slice`. This is cheaper than any deep-copy method and is sufficient when nested objects are either immutable or not being mutated.
+2. **Does the object contain functions, methods, or class instances whose prototype must be preserved?** Use Lodash's `_.cloneDeep`. It is a widely-used option that copies function-valued properties by reference and keeps the prototype chain of class instances intact; `structuredClone()` throws `DataCloneError` on function- or symbol-valued properties, and returns plain objects for class instances. Two caveats: `_.cloneDeep(fn)` returns `{}` when the top-level argument is itself a function (this branch only helps when the function lives inside the object being cloned), and neither method re-runs constructors, so any invariants enforced only at construction time are not reapplied to the clone.
+3. **Does the object contain `Date`, `Map`, `Set`, `RegExp`, typed arrays, `ArrayBuffer`, or circular references?** Use `structuredClone()`. It is built in, dependency-free, and handles all of these natively; `JSON.parse(JSON.stringify())` silently loses or throws on each of them.
+4. **Is every value strictly JSON-safe (strings, finite numbers, booleans, `null`, plain objects and arrays — no `undefined`, `NaN`/`Infinity`, functions, symbols, or `BigInt`)?** Either `structuredClone()` or `JSON.parse(JSON.stringify(obj))` works. Prefer `structuredClone()` as a default — it also handles `Date`, `Map`, `Set`, `RegExp`, typed arrays, and cycles if those later appear in the object, though it will still throw on functions and symbols.
+
+In short: `structuredClone()` is the default deep-copy in modern JavaScript. Reach for `_.cloneDeep` when functions or prototypes matter, and reserve `JSON.parse(JSON.stringify())` for short scripts where the input is known to be JSON-safe.
+
+## Edge cases worth knowing
+
+### Shallow copy only duplicates the top level
+
+```js live
+const user = { name: 'Ada', address: { city: 'London' } };
+const copy = { ...user };
+
+copy.name = 'Grace';
+copy.address.city = 'Paris';
+
+console.log(user.name); // 'Ada'
+console.log(user.address.city); // 'Paris'
+```
+
+`name` is a primitive, so assigning to `copy.name` does not affect `user.name`. `address` is an object, and the spread operator copies the reference — `copy.address` and `user.address` point to the same object, so mutating one is visible through the other.
+
+### `JSON.parse(JSON.stringify())` data loss
+
+```js live
+const data = {
+ createdAt: new Date('2026-01-01'),
+ score: NaN,
+ note: undefined,
+ tags: new Set(['a', 'b']),
+};
+
+const cloned = JSON.parse(JSON.stringify(data));
+console.log(cloned);
+// {
+// createdAt: '2026-01-01T00:00:00.000Z', // Date serialized as ISO string
+// score: null, // NaN serialized as null
+// tags: {} // Set has no enumerable own properties
+// }
+// `note` is absent — undefined values are omitted by JSON.stringify.
+```
+
+`JSON.stringify` calls `toJSON()` (on `Date`) or returns `null` (on `NaN`, `Infinity`), and skips `undefined` values and function-valued properties entirely. `Map` and `Set` have no enumerable own properties, so they serialize to `{}`.
+
+### `structuredClone()` and circular references
+
+```js live
+const a = { name: 'a' };
+a.self = a;
+
+const cloned = structuredClone(a);
+console.log(cloned.self === cloned); // true
+console.log(cloned.self === a); // false
+```
+
+`structuredClone()` tracks visited references during cloning and reconstructs cycles in the output. `JSON.parse(JSON.stringify())` throws `TypeError: Converting circular structure to JSON` on the same input.
+
+### `structuredClone()` drops the prototype chain
+
+```js live
+class User {
+ constructor(name) {
+ this.name = name;
+ }
+ greet() {
+ return `Hi, ${this.name}`;
+ }
+}
+
+const u = new User('Ada');
+const cloned = structuredClone(u);
+
+console.log(cloned instanceof User); // false
+console.log(cloned.greet); // undefined
+```
+
+The structured clone algorithm copies own data properties only; the result has `Object.prototype` as its prototype. To clone class instances with methods and `instanceof` intact, use Lodash's `_.cloneDeep`, or define an explicit `clone()` method on the class.
+
+## Shallow copies and React state updates
+
+React compares next and previous state with [`Object.is`](https://react.dev/reference/react/useState#setstate-caveats) and bails out of re-rendering if the reference is unchanged. Mutating a nested value and calling the setter with the same top-level reference therefore has no effect on the rendered output.
+
+```jsx
+function UserProfile() {
+ const [user, setUser] = useState({
+ name: 'Ada',
+ preferences: { theme: 'dark' },
+ });
+
+ const toggleTheme = () => {
+ // Mutates the existing nested object and passes the same top-level reference.
+ user.preferences.theme =
+ user.preferences.theme === 'dark' ? 'light' : 'dark';
+ setUser(user); // Object.is(user, user) === true — React bails out.
+ };
+
+ return ;
+}
+```
+
+Spreading the top level while still mutating the nested object is also incorrect:
+
+```jsx
+const toggleTheme = () => {
+ user.preferences.theme = user.preferences.theme === 'dark' ? 'light' : 'dark';
+ // `user.preferences` was mutated in place on the line above. Any previous
+ // snapshot, memoized selector, or component still holding the old reference
+ // now observes the toggled value.
+ setUser({ ...user, preferences: { ...user.preferences } });
+};
+```
+
+The correct pattern computes the new value while constructing the new object tree, without mutation:
+
+```jsx
+setUser((prev) => ({
+ ...prev,
+ preferences: {
+ ...prev.preferences,
+ theme: prev.preferences.theme === 'dark' ? 'light' : 'dark',
+ },
+}));
```
-In this example, `deepCopy` is a deep copy of `obj1`. Changing `deepCopy.b.c` does not affect `obj1.b.c` because `b` is a completely new object in `deepCopy`.
+Manual spread chains become error-prone past two levels of nesting. Libraries such as [Immer](https://immerjs.github.io/immer/) (used internally by Redux Toolkit) let the developer write mutation-style code against a draft, which the library converts into an immutable update.
+
+## Follow-up questions
+
+### Why can't `JSON.parse(JSON.stringify())` round-trip `Date` objects?
+
+`JSON.stringify` invokes `Date.prototype.toJSON`, which returns the ISO string form of the date. The resulting JSON contains no type information, so `JSON.parse` returns a plain string. Any subsequent call such as `.getTime()` or a date-formatting operation on the parsed value will throw `TypeError`, because the value is a string, not a `Date`. This is a common source of bugs in flows that serialize state to `localStorage` and rehydrate it on load.
-### Methods to create shallow and deep copies
+### How can an object containing a `Map` be deep-cloned?
-#### Shallow copy methods
+`structuredClone(obj)` handles `Map` natively and is the default in any modern environment. Lodash's `_.cloneDeep` is the fallback when the object also contains class instances or functions, or when running on a runtime that predates `structuredClone`. `JSON.parse(JSON.stringify())` is not viable — `Map` entries are not enumerable own properties, so a `Map` serializes to `{}`.
-- `Object.assign()`
-- Spread operator (`...`)
+### When should `_.cloneDeep` be used instead of `structuredClone`?
-#### Deep copy methods
+The two main cases — class instances whose prototype must survive, and objects with function-valued properties — are covered in the [decision guide above](#which-method-should-i-reach-for). The third case specific to Lodash is **custom per-type cloning**: `_.cloneDeepWith(obj, customizer)` takes a callback that intercepts specific types, useful for framework-specific objects or domain models with non-enumerable state.
-- `JSON.parse(JSON.stringify())`
-- `structuredClone()` (modern JavaScript)
-- Libraries like `Lodash` (`_.cloneDeep`)
+### Is the spread operator always a shallow copy?
+
+Yes. The spread operator copies own enumerable properties one level deep. When spreading a class instance, inherited methods on the prototype are not included in the result, and the output is a plain object rather than an instance of the original class.
## Further reading
+- [MDN Web Docs: structuredClone()](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone)
- [MDN Web Docs: Object.assign()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)
- [MDN Web Docs: JSON.parse()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse)
-- [MDN Web Docs: structuredClone()](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone)
- [Lodash Documentation: \_.cloneDeep](https://lodash.com/docs/4.17.15#cloneDeep)
+- [React docs: Updating objects in state](https://react.dev/learn/updating-objects-in-state)
diff --git a/questions/explain-what-a-single-page-app-is-and-how-to-make-one-seo-friendly/en-US.mdx b/questions/explain-what-a-single-page-app-is-and-how-to-make-one-seo-friendly/en-US.mdx
index 59c8a84..e0b9b40 100644
--- a/questions/explain-what-a-single-page-app-is-and-how-to-make-one-seo-friendly/en-US.mdx
+++ b/questions/explain-what-a-single-page-app-is-and-how-to-make-one-seo-friendly/en-US.mdx
@@ -4,118 +4,189 @@ title: Explain what a single page app is and how to make one SEO-friendly
## TL;DR
-A single page application (SPA) is a web application that loads a single HTML page and dynamically updates content as the user interacts with the app. This approach provides a more fluid user experience but can be challenging for SEO because search engines may not execute JavaScript to render content. To make an SPA SEO-friendly, you can use server-side rendering (SSR) or static site generation (SSG) to ensure that search engines can index your content. Tools like Next.js for React or Nuxt.js for Vue.js can help achieve this.
+A single page application (SPA) is a web application that loads a single HTML document and updates its content in the browser via JavaScript, rather than requesting a new page from the server on each navigation. This model provides application-like UX but presents challenges for search engine indexing because the initial HTML does not contain the rendered content.
+
+In current practice, SPAs are made SEO-friendly by producing HTML on the server rather than relying solely on client-side rendering. The available strategies are server-side rendering (SSR), static site generation (SSG), incremental static regeneration (ISR), and streaming with React Server Components. Each strategy offers a different tradeoff between freshness, server cost, and time to first paint, and modern frameworks such as Next.js, Nuxt, Remix, SvelteKit, and Astro support selecting the strategy per route.
---
## What is a single page app?
-### Definition
+A SPA is a web application that loads a single HTML document and performs navigation and content updates on the client via JavaScript. The initial page load retrieves the HTML shell and application bundle; subsequent user actions update the DOM in place rather than triggering full-page navigations.
-A single page application (SPA) is a web application that interacts with the user by dynamically rewriting the current page rather than loading entire new pages from the server. This results in a more fluid user experience, similar to a desktop application.
+Key characteristics:
-### Key characteristics
+- A single HTML document is served on initial load; subsequent views are rendered client-side.
+- The `fetch` API (or `XMLHttpRequest`) is used to communicate with the server without full-page reloads.
+- A client-side router (for example, `react-router` or `vue-router`) maps URL changes to view transitions.
+- Application state is typically held in memory rather than stored per request.
-- The application loads a single HTML page and dynamically updates it as the user interacts with the app
-- Uses AJAX or Fetch API to communicate with the server and update the page without a full reload
-- Often relies on client-side routing to manage different views or states within the app
+Benefits:
-### Benefits
+- Smoother navigation after the initial load.
+- Reduced server load, because subsequent navigations do not require rendering a new page.
+- Application-like interaction patterns, such as preserved state across route transitions.
-- Faster interactions after the initial load
-- Reduced server load due to fewer full-page requests
-- Improved user experience with smoother transitions
+## How search engines render JavaScript
-## How to make an SPA SEO-friendly
+The statement "Google cannot index JavaScript" is out of date. The practical situation is more nuanced:
-### Challenges
+- **Googlebot executes JavaScript** using an evergreen Chromium-based renderer. The bot fetches HTML first, and pages requiring JavaScript rendering are added to a separate render queue. The render queue has improved substantially since 2019, but indexing of JS-rendered content is typically slower than indexing of server-rendered HTML.
+- **Rendering is separate from crawling.** This two-phase model means JS-rendered content can appear in the index later than its server-rendered counterpart, which is a disadvantage for time-sensitive content or highly competitive queries.
+- **Other search engines render JavaScript less reliably.** Bing's JavaScript rendering is less consistent than Google's, and other engines such as Yandex and Baidu have more limited support. SSR or prerendering is therefore still relevant when non-Google traffic is important.
+- **Social and preview scrapers do not execute JavaScript.** Clients that fetch OpenGraph and Twitter Card metadata — including Slack, LinkedIn, Discord, and Facebook — read the initial HTML only. Meta tags that are injected by client-side code are not visible to these clients.
-SPAs can be challenging for SEO because search engines may not execute JavaScript to render content. This can result in search engines indexing an empty or incomplete page.
+For details, see Google's [JavaScript SEO documentation](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) and the [Google Search Central documentation on rendering](https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering).
-### Solutions
+## Rendering strategies
-#### Server-side rendering (SSR)
+Modern frameworks allow different strategies to be used on different routes within the same application.
-Server-side rendering involves rendering the initial HTML of the page on the server before sending it to the client. This ensures that search engines can index the fully rendered content.
+### Server-side rendering (SSR)
-- **React**: Use Next.js, which provides built-in support for SSR
-- **Vue.js**: Use Nuxt.js, which also supports SSR out of the box
+The server renders the HTML for each request using the current application state. This produces indexable HTML on first response and reflects up-to-date data, at the cost of per-request server rendering.
-Example with Next.js:
+Example with the Next.js App Router:
-```javascript
-import React from 'react';
-import { GetServerSideProps } from 'next';
+```jsx
+// app/products/[id]/page.tsx — a React Server Component, async by default
+export default async function ProductPage({ params }) {
+ const res = await fetch(`https://api.example.com/products/${params.id}`, {
+ cache: 'no-store',
+ });
+ const product = await res.json();
-const Page = ({ data }) => {
return (
-
{data.title}
-
{data.content}
+
{product.title}
+
{product.description}
);
-};
+}
+```
+
+The App Router, introduced in Next.js 13, replaces the `getServerSideProps` data-fetching function of the Pages Router with async Server Components. `cache: 'no-store'` opts out of the framework's data cache to produce a fresh render on each request.
-export const getServerSideProps: GetServerSideProps = async () => {
- const res = await fetch('https://api.example.com/data');
- const data = await res.json();
+### Static site generation (SSG)
- return {
- props: {
- data,
- },
- };
-};
+HTML is produced at build time and served as static files. This is the cheapest option to host, produces the fastest first-byte response, and is suitable for content that does not change per request.
-export default Page;
+```jsx
+// app/blog/[slug]/page.tsx
+export async function generateStaticParams() {
+ const posts = await fetch('https://api.example.com/posts').then((r) =>
+ r.json(),
+ );
+ return posts.map((p) => ({ slug: p.slug }));
+}
+
+export default async function Post({ params }) {
+ const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(
+ (r) => r.json(),
+ );
+ return {post.body};
+}
```
-#### Static site generation (SSG)
+### Incremental static regeneration (ISR)
-Static site generation involves generating the HTML for each page at build time. This approach is suitable for content that doesn't change frequently.
+Pages are generated statically and cached, with a configurable revalidation interval. The cached HTML is served immediately; when the revalidation interval expires, the framework regenerates the page in the background on the next request. This combines SSG's serving cost with tunable freshness.
-- **React**: Use Next.js with its static generation capabilities
-- **Vue.js**: Use Nuxt.js with its static site generation feature
+```jsx
+// app/categories/[slug]/page.tsx
+export default async function Category({ params }) {
+ const res = await fetch(`https://api.example.com/categories/${params.slug}`, {
+ next: { revalidate: 3600 },
+ });
+ const category = await res.json();
+ return ;
+}
+```
+
+### React Server Components with streaming
-Example with Next.js:
+React Server Components execute on the server and ship as a serialized tree rather than HTML. Combined with `Suspense` boundaries, the framework can stream the HTML shell early and defer slower sections until their data resolves. This is useful for pages with a fast critical section and slower below-the-fold content.
-```javascript
-import React from 'react';
-import { GetStaticProps } from 'next';
+```jsx
+import { Suspense } from 'react';
-const Page = ({ data }) => {
+export default function Feed() {
return (
-
-
{data.title}
-
{data.content}
-
+ <>
+
+ }>
+
+
+ >
);
-};
+}
+```
-export const getStaticProps: GetStaticProps = async () => {
- const res = await fetch('https://api.example.com/data');
- const data = await res.json();
+### Client-side rendering (CSR)
- return {
- props: {
- data,
- },
- };
-};
+The HTML shell is served, and the full view is rendered by JavaScript in the browser. This remains appropriate for views that are not intended to be indexed, such as authenticated dashboards and internal tools, where SSR adds server cost without SEO benefit.
-export default Page;
-```
+## Choosing a strategy per route
+
+The appropriate strategy depends on the route's data characteristics and indexing requirements. A typical allocation is:
+
+| Use case | Recommended strategy | Rationale |
+| --- | --- | --- |
+| E-commerce product page | ISR, short revalidation window | Prices and inventory change, but not per visit; cached HTML improves Largest Contentful Paint |
+| Marketing site, documentation, blog | SSG | No per-request variability; suitable for CDN distribution |
+| Dashboard behind authentication | CSR | Not indexed; SSR provides no SEO benefit and increases server cost |
+| Personalized feed or homepage | RSC with streaming | Fast shell response; personalized content is streamed as it resolves |
+| Search results page | SSR | Query-dependent output that should be indexable for long-tail queries |
+| Real-time dashboard | CSR | Data changes more frequently than server HTML can be regenerated usefully |
+| Breaking news article | SSR or short-window ISR | Freshness is important; SSR under traffic spikes, ISR otherwise |
+
+## Core Web Vitals comparison
+
+The rendering strategy affects the metrics used by search ranking and perceived performance. The following table gives illustrative ranges for a content page with approximately 100 KB of data, measured on a slow mobile connection. Actual values depend on payload size, server region, and CDN configuration, and should be measured against the specific deployment.
+
+| Strategy | TTFB | LCP | Notes |
+| --- | --- | --- | --- |
+| CSR | Low | High | Fast first byte (shell only); LCP blocked by JS download and API round trip |
+| SSR | Moderate | Low | Slower first byte due to server rendering; content visible sooner |
+| SSG | Low | Low | CDN-cached HTML; typically the fastest overall |
+| ISR | Low | Low | Served from CDN cache; regenerated in background on revalidation |
+| RSC streaming | Low | Low | Shell streams first; Suspense boundaries hydrate as data resolves |
+
+Tools such as [PageSpeed Insights](https://pagespeed.web.dev/) and WebPageTest provide lab measurements against a specific URL.
+
+## Framework coverage
+
+Several frameworks support the strategies above:
+
+- **[Next.js](https://nextjs.org/)** (React) — App Router is the current default. Supports SSR, SSG, ISR, and React Server Components with streaming.
+- **[Remix](https://remix.run/)** (React) — Emphasizes web standards and nested routing with loader and action functions. Merged with React Router in 2024.
+- **[Nuxt](https://nuxt.com/)** (Vue) — Supports SSR, SSG, ISR (via the Nitro server), and hybrid rendering.
+- **[SvelteKit](https://kit.svelte.dev/)** (Svelte) — Adapter-based; deploys as SSR, SSG, or edge functions depending on configuration.
+- **[Astro](https://astro.build/)** — Island architecture. Ships zero JavaScript by default and hydrates only the components marked as interactive. Well suited to content-heavy sites with limited interactivity.
+- **[SolidStart](https://start.solidjs.com/)** (Solid) — Architecturally similar to SvelteKit with Solid's fine-grained reactivity.
+
+General guidance for new projects:
+
+- Content-heavy sites with limited interactivity: Astro.
+- React applications with a mix of interactive and indexable routes: Next.js or Remix.
+- Vue applications: Nuxt.
-#### Pre-rendering with tools
+A framework with SSR or SSG support is preferable to pure client-side rendering when SEO is a requirement, even for applications that would otherwise be implemented as a traditional SPA.
-Some tools can pre-render your SPA and serve the pre-rendered HTML to search engines.
+## Common misconceptions
-- **Prerender.io**: A service that pre-renders your JavaScript application and serves the static HTML to search engines
-- **Rendertron**: A headless Chrome rendering solution that can be used to pre-render your SPA
+1. **"SSR means no JavaScript on the client."** SSR produces server-rendered HTML, but the client still downloads and hydrates the JavaScript bundle to attach event handlers. The bundle size is generally comparable to the CSR equivalent.
+2. **"CSR is not SEO-friendly."** Googlebot indexes content rendered by JavaScript. The practical concerns are latency, indexing reliability, and compatibility with non-Google engines and social scrapers. For high-competition queries these concerns are significant; for long-tail content they may be acceptable.
+3. **"SSR should be used for everything."** SSR has a per-request CPU cost. For content that does not vary per request, SSG is substantially cheaper to serve. Defaulting to SSR when SSG would suffice increases hosting cost without benefit.
+4. **"Hydration is free."** Hydration re-executes the component tree on the client to attach event handlers. Large hydration trees can affect Interaction to Next Paint (INP) and other interactivity metrics. React Server Components reduce hydration cost by allowing portions of the tree to remain server-only.
## Further reading
-- [Next.js Documentation](https://nextjs.org/docs)
-- [Nuxt.js Documentation](https://nuxtjs.org/docs)
-- [Prerender.io](https://prerender.io/)
-- [Rendertron](https://github.com/GoogleChrome/rendertron)
+- [Next.js App Router documentation](https://nextjs.org/docs/app)
+- [React Server Components overview](https://react.dev/reference/rsc/server-components)
+- [Google Search Central: JavaScript SEO basics](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics)
+- [web.dev: Rendering on the Web](https://web.dev/articles/rendering-on-the-web)
+- [Remix Documentation](https://remix.run/docs)
+- [Astro Documentation](https://docs.astro.build/)
+- [Nuxt Documentation](https://nuxt.com/docs)
+- [SvelteKit Documentation](https://kit.svelte.dev/docs/introduction)
diff --git a/questions/what-is-a-closure-and-how-why-would-you-use-one/en-US.mdx b/questions/what-is-a-closure-and-how-why-would-you-use-one/en-US.mdx
index f044dcc..5484265 100644
--- a/questions/what-is-a-closure-and-how-why-would-you-use-one/en-US.mdx
+++ b/questions/what-is-a-closure-and-how-why-would-you-use-one/en-US.mdx
@@ -63,6 +63,50 @@ console.log(counter()); // Outputs: 1
console.log(counter()); // Outputs: 2
```
+## Closures compared with classes
+
+Closures and classes can both encapsulate state and expose operations on it. The two approaches differ in privacy mechanism, memory characteristics, and idiomatic fit.
+
+```js live
+// Closure-based implementation
+function makeCounter() {
+ let count = 0;
+ return {
+ inc: () => ++count,
+ get: () => count,
+ };
+}
+
+// Class-based implementation with private fields
+class Counter {
+ #count = 0;
+ inc() {
+ return ++this.#count;
+ }
+ get() {
+ return this.#count;
+ }
+}
+
+const a = makeCounter();
+const b = new Counter();
+console.log(a.inc(), a.inc()); // 1 2
+console.log(b.inc(), b.inc()); // 1 2
+```
+
+| Concern | Closure | Class with `#private` fields |
+| --- | --- | --- |
+| Privacy | Lexical scope; inaccessible from outside the closure | Private slot; access outside the class throws `TypeError` |
+| Memory per instance | New closure scope and new function objects per call | Instance state per `new`; methods shared via the prototype |
+| `this` binding | Not required; methods close over outer variables | Methods use `this`; additional care required for callbacks |
+| Prototype sharing | Not supported; each instance has its own methods | Supported; instance methods share the prototype |
+| Typical use | Factories, event handlers, partial application, FP | Long-lived domain objects, inheritance hierarchies |
+
+General guidance:
+
+- A closure is appropriate when a small number of instances with encapsulated state are needed and inheritance is not a concern.
+- A class is appropriate when many instances share the same behavior (prototype sharing avoids duplicating methods per instance), when `instanceof` checks or inheritance are needed, or when the API is consumed through type annotations in TypeScript.
+
## Closures in React
Closures are everywhere. The code below shows a simple example of increasing a counter on a button click. In this code, `handleClick` forms a closure. It has access to its outer scope variables `count` and `setCount`.
@@ -71,12 +115,10 @@ Closures are everywhere. The code below shows a simple example of increasing a c
import React, { useState } from 'react';
function Counter() {
- // Define a state variable using the useState hook
const [count, setCount] = useState(0);
- // This handleClick function is a closure
+ // `handleClick` is a closure over `count` and `setCount`.
function handleClick() {
- // It can access the 'count' state variable
setCount(count + 1);
}
@@ -87,19 +129,138 @@ function Counter() {
);
}
+```
-function App() {
- return (
-
-
Counter App
-
-
- );
+### Stale closures in `useEffect`
+
+A closure inside a `useEffect` callback captures the values of the variables it references at the time the effect runs. When those values change on subsequent renders, the closure continues to reference the originally captured values unless the effect re-runs or a different mechanism is used to read live state. This is a common cause of hooks-related bugs.
+
+```jsx
+function Chat() {
+ const [count, setCount] = useState(0);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ // `count` here refers to the value captured when the effect ran.
+ // With an empty dependency array, the effect runs only once, so this
+ // value is always 0.
+ console.log('current count:', count);
+ setCount(count + 1);
+ }, 1000);
+ return () => clearInterval(interval);
+ }, []);
+
+ return
{count}
;
+}
+```
+
+There are three ways to correct this:
+
+**Declare the dependency.** The effect re-runs whenever `count` changes, and a new closure captures the current value:
+
+```jsx
+useEffect(() => {
+ const interval = setInterval(() => {
+ setCount(count + 1);
+ }, 1000);
+ return () => clearInterval(interval);
+}, [count]);
+```
+
+This is correct, but creates a new interval every second.
+
+**Use the functional updater form of `setState`.** The updater receives the current state as an argument, so no value needs to be captured:
+
+```jsx
+useEffect(() => {
+ const interval = setInterval(() => {
+ setCount((prev) => prev + 1);
+ }, 1000);
+ return () => clearInterval(interval);
+}, []);
+```
+
+**Use a ref.** Useful when the callback should read live state but not re-run when the state changes:
+
+```jsx
+const countRef = useRef(0);
+useEffect(() => {
+ countRef.current = count;
+});
+
+useEffect(() => {
+ const interval = setInterval(() => {
+ console.log('current count:', countRef.current);
+ }, 1000);
+ return () => clearInterval(interval);
+}, []);
+```
+
+The functional updater is usually the simplest correct option when the callback only needs to update state. The ref approach is appropriate when live state must be read without re-running the subscription.
+
+## Memoization with closures
+
+Function memoization caches results of a computation against the arguments used to produce them. A closure holds the cache, keeping it scoped to the memoized wrapper.
+
+```js live
+function memoize(fn) {
+ const cache = new Map();
+ return function (...args) {
+ const key = JSON.stringify(args);
+ if (cache.has(key)) return cache.get(key);
+ const result = fn.apply(this, args);
+ cache.set(key, result);
+ return result;
+ };
}
-export default App;
+const slowSquare = (n) => {
+ console.log('computing', n);
+ return n * n;
+};
+
+const fastSquare = memoize(slowSquare);
+console.log(fastSquare(4)); // 'computing 4' then 16
+console.log(fastSquare(4)); // 16 (cache hit; no 'computing' log)
+console.log(fastSquare(5)); // 'computing 5' then 25
```
+Observations:
+
+1. The `cache` variable is accessible only through the returned function. This is the "private state" property of closures applied to a practical utility.
+2. The cache grows unbounded by default, which can cause memory issues on long-running inputs. Production memoization implementations typically use an LRU cache or a `WeakMap` keyed on object identity. Further discussion of this and related issues is on the [closure pitfalls page](/questions/quiz/what-are-the-potential-pitfalls-of-using-closures).
+3. The same pattern appears in widely used utilities, including React's `useMemo` and `useCallback`, Reselect's `createSelector`, and Lodash's `_.memoize`.
+
+## Common example: `var` in a loop
+
+```js live
+for (var i = 0; i < 3; i++) {
+ setTimeout(() => console.log(i), 0);
+}
+// Output: 3, 3, 3
+```
+
+`var i` is function-scoped, so all three callbacks close over the same binding. The loop completes and sets `i` to `3` before any `setTimeout` callback runs, because `setTimeout(fn, 0)` still queues the callback as a macrotask that runs after the current synchronous code.
+
+Two corrections:
+
+- Replace `var` with `let`. `let` is block-scoped, so each iteration creates a fresh binding, and each callback closes over a different value.
+- Use an IIFE to create a new function scope per iteration: `(i => setTimeout(() => console.log(i), 0))(i)`. This is the pre-ES6 alternative.
+
+## Common questions
+
+### When is a closure preferable to a class?
+
+A closure is generally preferable when the expected number of instances is small, when inheritance and `instanceof` are not needed, and when avoiding `this` is desirable. A class is preferable when many instances will share the same behavior (prototype sharing), when inheritance is used, or when the API is consumed through TypeScript type annotations.
+
+### Can closures cause memory leaks?
+
+Yes. A closure that references a large object and is itself reachable from long-lived state (module scope, event listener registrations, or a Redux store, for example) will keep the referenced object alive. The [closure pitfalls page](/questions/quiz/what-are-the-potential-pitfalls-of-using-closures) covers specific patterns and how to detect them.
+
+### Are closures synchronous or asynchronous?
+
+A closure is simply a function that captures its lexical scope. The function may be invoked synchronously or asynchronously; the closure mechanism itself is independent of the invocation timing. The `var`-in-loop example above is a common source of confusion because the variable referenced by the closure changes between the closure's creation and its asynchronous invocation.
+
## Why use closures?
Using closures provides the following benefits:
@@ -109,9 +270,14 @@ Using closures provides the following benefits:
1. **Event handlers and callbacks**: In JavaScript, closures are often used in event handlers and callbacks to maintain state or access variables that were in scope when the handler or callback was defined.
1. **Module patterns**: Closures enable the [module pattern](https://www.patterns.dev/vanilla/module-pattern) in JavaScript, allowing the creation of modules with private and public parts.
+## Related questions
+
+You can read more about [memory leaks and other pitfalls of closures](/questions/quiz/what-are-the-potential-pitfalls-of-using-closures) and the [module pattern and private state](/questions/quiz/how-can-closures-be-used-to-create-private-variables) on their dedicated pages.
+
## Further reading
- [Closures - MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures)
- [Closures - Javascript.info](https://javascript.info/closure)
- [Closure - Eloquent Javascript](https://eloquentjavascript.net/03_functions.html)
- [You Don't Know JS Yet: Scope & Closures](https://github.com/getify/You-Dont-Know-JS/tree/2nd-ed/scope-closures)
+- [React: Referencing values with refs](https://react.dev/learn/referencing-values-with-refs)
diff --git a/questions/what-is-event-loop-what-is-the-difference-between-call-stack-and-task-queue/en-US.mdx b/questions/what-is-event-loop-what-is-the-difference-between-call-stack-and-task-queue/en-US.mdx
index c7ccaa1..0c34fdd 100644
--- a/questions/what-is-event-loop-what-is-the-difference-between-call-stack-and-task-queue/en-US.mdx
+++ b/questions/what-is-event-loop-what-is-the-difference-between-call-stack-and-task-queue/en-US.mdx
@@ -11,25 +11,17 @@ The event loop is a concept within the JavaScript runtime environment regarding
2. When an asynchronous operation is encountered (e.g., `setTimeout()`, HTTP request), it is offloaded to the respective Web API or Node.js API to handle the operation in the background.
3. Once the asynchronous operation completes, its callback function is placed in the respective queues – task queues (also known as macrotask queues / callback queues) or microtask queues. We will refer to "task queue" as "macrotask queue" from here on to better differentiate from the microtask queue.
4. The event loop continuously monitors the call stack and executes items on the call stack. If/when the call stack is empty:
- 1. Microtask queue is processed. Microtasks include promise callbacks (`then`, `catch`, `finally`), `MutationObserver` callbacks, and calls to `queueMicrotask()`. The event loop takes the first callback from the microtask queue and pushes it to the call stack for execution. This repeats until the microtask queue is empty.
+ 1. Microtask queue is processed. Microtasks include promise callbacks (`then`, `catch`, `finally`), `await` continuations, `MutationObserver` callbacks, and calls to `queueMicrotask()`. The event loop takes the first callback from the microtask queue and pushes it to the call stack for execution. This repeats until the microtask queue is empty.
2. Macrotask queue is processed. Macrotasks include web APIs like `setTimeout()`, HTTP requests, user interface event handlers like clicks, scrolls, etc. The event loop dequeues the first callback from the macrotask queue and pushes it onto the call stack for execution. However, after a macrotask queue callback is processed, the event loop does not proceed with the next macrotask yet! The event loop first checks the microtask queue. Checking the microtask queue is necessary as microtasks have higher priority than macrotask queue callbacks. The macrotask queue callback that was just executed could have added more microtasks!
1. If the microtask queue is non-empty, process them as per the previous step.
2. If the microtask queue is empty, the next macrotask queue callback is processed. This repeats until the macrotask queue is empty.
5. This process continues indefinitely, allowing the JavaScript engine to handle both synchronous and asynchronous operations efficiently without blocking the call stack.
-The unfortunate truth is that it is extremely hard to explain the event loop well using only text. We recommend checking out one of the following excellent videos explaining the event loop:
-
-- [JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue](https://www.youtube.com/watch?v=eiC58R16hb8) (2024): Lydia Hallie is a popular educator on JavaScript and this is the best recent video explaining the event loop. There's also an [accompanying blog post](https://www.lydiahallie.com/blog/event-loop) for those who prefer detailed text-based explanations.
-- [In the Loop](https://www.youtube.com/watch?v=cCOL7MC4Pl0) (2018): Jake Archibald previously from the Chrome team provides a visual demonstration of the event loop during JSConf 2018, accounting for different types of tasks.
-- [What the heck is the event loop anyway?](https://www.youtube.com/watch?v=8aGhZQkoFbQ) (2014): Philip Roberts gave this epic talk at JSConf 2014 and it is one of the most viewed JavaScript videos on YouTube.
-
-We recommend watching [Lydia's video](https://www.youtube.com/watch?v=eiC58R16hb8) as it is the most modern and concise explanation standing at only 13 minutes long whereas the other videos are at least 30 minutes long. Her video is sufficient for the purpose of interviews.
-
---
## Event loop in JavaScript
-The event loop is the heart of JavaScript's asynchronous operation. It is a mechanism that handles the execution of code, allowing for asynchronous operations and ensuring that the single-threaded nature of JavaScript engines does not block the execution of the program.
+The event loop is the mechanism that lets JavaScript handle asynchronous operations without blocking its single-threaded execution.
### Parts of the event loop
@@ -45,11 +37,11 @@ Asynchronous operations like `setTimeout()`, HTTP requests, file I/O, etc., are
#### Task queue / Macrotask queue / Callback queue
-The task queue, also known as the macrotask queue / callback queue / event queue, is a queue that holds tasks that need to be executed. These tasks are typically asynchronous operations, such as callbacks passed to web APIs (`setTimeout()`, `setInterval()`, HTTP requests, etc.), and user interface event handlers like clicks, scrolls, etc.
+The macrotask queue (also called the task queue, callback queue, or event queue) holds callbacks waiting to run when the call stack and microtask queue are empty.
#### Microtasks queue
-Microtasks are tasks that have a higher priority than macrotasks and are executed immediately after the currently executing script is completed and before the next macrotask is executed. Microtasks are usually used for more immediate, lightweight operations that should be executed as soon as possible after the current operation completes. There is a dedicated microtask queue for microtasks. Microtasks include promise callbacks (`then()`, `catch()`, and `finally()`), `await` statements, `queueMicrotask()`, and `MutationObserver` callbacks.
+The microtask queue holds higher-priority callbacks that drain after the call stack empties and between every macrotask.
### Event loop order
@@ -65,22 +57,22 @@ Microtasks are tasks that have a higher priority than macrotasks and are execute
### Example
-The following code logs some statements using a combination of normal execution, macrotasks, and microtasks.
+The example below mixes synchronous logs with two timer callbacks and two promise callbacks. The first timer's callback enqueues a microtask, and the first promise callback enqueues another timer — small additions that exercise every ordering rule the event loop applies, while keeping each line individually trivial to read.
```js live
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
+ Promise.resolve().then(() => console.log('Promise 2'));
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
+ setTimeout(() => console.log('Timeout 3'), 0);
});
-setTimeout(() => {
- console.log('Timeout 2');
-}, 0);
+setTimeout(() => console.log('Timeout 2'), 0);
console.log('End');
@@ -89,21 +81,299 @@ console.log('End');
// End
// Promise 1
// Timeout 1
+// Promise 2
// Timeout 2
+// Timeout 3
+```
+
+Queue entries in the trace below are labeled by the message their callback will log (so `[Promise 1]` means "the queued callback that will log `Promise 1`"). Names match registration order: `Timeout 1` is the first timer registered, `Promise 2` is the microtask scheduled later by `Timeout 1`'s callback, and so on.
+
+| Step | What just happened | Call stack | Microtask queue | Macrotask queue | Output |
+| --- | --- | --- | --- | --- | --- |
+| 1 | `console.log('Start')` runs | empty | empty | empty | `Start` |
+| 2 | The first `setTimeout` registers a timer with the Web API | empty | empty | empty | `Start` |
+| 3 | `Promise.resolve().then(...)` enqueues its callback as a microtask | empty | `[Promise 1]` | empty | `Start` |
+| 4 | The second `setTimeout` registers another timer | empty | `[Promise 1]` | empty | `Start` |
+| 5 | `console.log('End')` runs; sync script finishes. Both 0 ms timers have elapsed and their callbacks have moved from the Web API into the macrotask queue, in registration order | empty | `[Promise 1]` | `[Timeout 1, Timeout 2]` | `Start, End` |
+| 6 | Stack empty → microtask queue drains: `Promise 1` runs and logs, then schedules a new timer whose callback will log `Timeout 3`. The new macrotask is appended to the end of the macrotask queue | empty | empty | `[Timeout 1, Timeout 2, Timeout 3]` | …, `Promise 1` |
+| 7 | Microtask queue empty → one macrotask runs: `Timeout 1` logs, then enqueues a new microtask that will log `Promise 2` | empty | `[Promise 2]` | `[Timeout 2, Timeout 3]` | …, `Timeout 1` |
+| 8 | Microtask queue is re-checked before the next macrotask (non-empty → drain): `Promise 2` runs and logs | empty | empty | `[Timeout 2, Timeout 3]` | …, `Promise 2` |
+| 9 | Microtask queue empty → next macrotask: `Timeout 2` runs and logs | empty | empty | `[Timeout 3]` | …, `Timeout 2` |
+| 10 | Microtask queue re-checked (empty) → next macrotask: `Timeout 3` runs and logs | empty | empty | empty | …, `Timeout 3` |
+
+Three rules the trace makes explicit:
+
+- **Microtasks drain before any macrotask.** Step 6 runs `Promise 1` before either timer, even though both timers were scheduled before the promise callback ran.
+- **A macrotask that schedules a microtask interleaves.** Step 7 runs `Timeout 1` and enqueues `Promise 2`; step 8 runs `Promise 2` _before_ the next macrotask, not after. The event loop re-checks the microtask queue between every macrotask, which is why a single drain at the end of synchronous code is not enough to model behavior correctly.
+- **A microtask that schedules a macrotask appends to the queue.** Step 6 runs `Promise 1` and schedules `Timeout 3`; `Timeout 3` then runs last, after both timers that were already in the macrotask queue. Microtasks cannot promote a macrotask to the front of the line.
+
+## Advanced examples
+
+The examples below demonstrate event loop behaviors that commonly appear in production code and more advanced interview questions.
+
+### `async`/`await` scheduling
+
+`async`/`await` is specified in terms of promise chaining. When execution reaches an `await`, the function is paused, its continuation is scheduled as a microtask on resolution of the awaited value, and control returns to the caller.
+
+```js live
+console.log('1');
+
+async function run() {
+ console.log('2');
+ await Promise.resolve();
+ console.log('3');
+}
+
+run();
+
+setTimeout(() => console.log('4'), 0);
+
+Promise.resolve().then(() => console.log('5'));
+
+console.log('6');
+
+// Output: 1, 2, 6, 3, 5, 4
+```
+
+Explanation:
+
+1. `1` is logged from the first synchronous statement.
+2. `run()` is invoked. Synchronous code in the function runs up to the `await`, logging `2`.
+3. The continuation of `run()` (everything after the `await`) is scheduled as a microtask. Control returns to the top-level script.
+4. `setTimeout` schedules a macrotask.
+5. `Promise.resolve().then(...)` schedules a microtask.
+6. `6` is logged from the last synchronous statement.
+7. The script completes and the microtask queue drains in FIFO order: `run()`'s continuation logs `3`, then the `.then` callback logs `5`.
+8. The macrotask queue is then processed, logging `4`.
+
+The common misconception is that `await` blocks execution. It does not — the function is paused, but control returns immediately to the caller, and the continuation runs as a microtask once the awaited value settles.
+
+### Microtask starvation
+
+Macrotasks run only once the microtask queue has fully drained. If microtasks continually schedule more microtasks, the macrotask queue never advances, which prevents rendering, user input handling, and timer callbacks from running.
+
+```js live
+let count = 0;
+
+function scheduleMicrotask() {
+ Promise.resolve().then(() => {
+ count++;
+ if (count < 5) scheduleMicrotask();
+ console.log('microtask', count);
+ });
+}
+
+setTimeout(() => console.log('macrotask fired'), 0);
+
+scheduleMicrotask();
+
+// Output: microtask 1, microtask 2, microtask 3, microtask 4, microtask 5, macrotask fired
+```
+
+With a bounded recursion depth, the macrotask eventually runs. An unbounded chain (for example `if (true)` instead of `if (count < 5)`) would prevent any macrotask from running and would block rendering in the browser.
+
+To yield to the browser for rendering or input handling, a macrotask is required — for example `setTimeout(fn, 0)`, `MessageChannel`, or `scheduler.yield()` in environments that support it. A microtask such as `queueMicrotask` or `Promise.resolve().then` does not yield.
+
+### Yielding the main thread to split long tasks
+
+A synchronous block that runs longer than 50 ms is classified as a [long task](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceLongTaskTiming) and blocks the browser from rendering, handling input, and processing timers for that duration. The fix is to break the work into chunks and yield to the event loop between chunks so that rendering and other macrotasks can run.
+
+A loop that runs as one task — the entire computation blocks until it finishes:
+
+```js
+function heavyWork() {
+ let sum = 0;
+ for (let i = 0; i < 1e8; i++) sum += i;
+ return sum;
+}
+
+heavyWork(); // ~hundreds of ms; the page is unresponsive for the duration
+```
+
+The same work split across macrotasks via `setTimeout`:
+
+```js live
+function chunked(total, chunkSize, onDone) {
+ let i = 0;
+ let sum = 0;
+ function tick() {
+ const end = Math.min(i + chunkSize, total);
+ while (i < end) {
+ sum += i;
+ i++;
+ }
+ if (i < total) {
+ setTimeout(tick, 0);
+ } else {
+ onDone(sum);
+ }
+ }
+ tick();
+}
+
+chunked(1e7, 1e6, (sum) => console.log('done', sum));
```
-Explanation of the output:
+Between every chunk, the browser can paint a frame, dispatch input events, and run other macrotasks. The drawback is that the HTML specification clamps nested `setTimeout` delays to a minimum of 4 ms after 5 levels of recursion, which adds noticeable latency to long chunked computations.
+
+`MessageChannel` schedules a macrotask without that clamp:
+
+```js live
+function yieldToMain() {
+ return new Promise((resolve) => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = () => resolve();
+ channel.port2.postMessage(null);
+ });
+}
+
+async function chunked(total, chunkSize) {
+ let i = 0;
+ let sum = 0;
+ while (i < total) {
+ const end = Math.min(i + chunkSize, total);
+ while (i < end) {
+ sum += i;
+ i++;
+ }
+ if (i < total) await yieldToMain();
+ }
+ return sum;
+}
+
+chunked(1e7, 1e6).then((sum) => console.log('done', sum));
+```
+
+`postMessage` enqueues a macrotask immediately without delay clamping, so the next chunk runs as soon as the browser has finished its render and any earlier pending tasks. React's scheduler used this pattern before `scheduler.postTask` was widely available. Production code should reuse a single `MessageChannel` instance instead of creating one per yield.
+
+A comparison of the available yielding mechanisms:
+
+| Mechanism | Schedule type | Yields to render? | Notes |
+| --- | --- | --- | --- |
+| `queueMicrotask` / `Promise.then` | Microtask | No | Drains before render — used for sequencing, not yielding |
+| `setTimeout(fn, 0)` | Macrotask | Yes | Clamped to ≥ 4 ms after 5 nested calls per the HTML specification |
+| `MessageChannel.postMessage` | Macrotask | Yes | No clamp; ~ 0 ms in practice |
+| `scheduler.postTask(fn, { priority })` | Macrotask | Yes | Built-in priority levels (`user-blocking`, `user-visible`, `background`); Chromium-only |
+| `scheduler.yield()` | Macrotask | Yes | Returns a promise that resolves on the next yield; preserves task continuation priority; Chromium-only |
+
+Microtasks cannot be used to yield. They drain before rendering, which is the behavior the microtask-starvation example demonstrates.
+
+### `queueMicrotask` compared to `Promise.resolve().then`
+
+Both schedule a microtask in the same FIFO queue and run at the same point in the event loop. They differ in how exceptions thrown inside the callback are surfaced.
+
+```js live
+queueMicrotask(() => {
+ throw new Error('from queueMicrotask');
+});
+
+Promise.resolve().then(() => {
+ throw new Error('from promise.then');
+});
+
+setTimeout(() => console.log('timeout ran'), 0);
+```
+
+- An exception thrown from a `queueMicrotask` callback is reported as an uncaught error and reaches `window.onerror` (in browsers) or `uncaughtException` (in Node).
+- An exception thrown from a `.then` callback causes the resulting promise to reject, surfacing through `unhandledrejection` if no downstream `.catch` handles it.
+
+`queueMicrotask` is appropriate when the callback is conceptually standalone and its errors should behave like any other thrown exception. `.then` is appropriate when the callback is part of a promise chain where errors are expected to be caught downstream.
+
+## Differences across runtimes
+
+The event loop is specified differently in browsers, Node.js, and Web Workers. Code that relies on precise scheduling may behave differently across these environments.
+
+### Browsers
+
+Specified in the [HTML Living Standard](https://html.spec.whatwg.org/multipage/webappapis.html#event-loops):
+
+- Each agent has its own event loop.
+- The macrotask queue is partitioned into multiple task sources (timers, network I/O, UI events, `postMessage`, and others). FIFO order is guaranteed within a source but not across sources — the user agent may choose any non-empty source each turn.
+- `requestAnimationFrame` callbacks run in a separate phase of the event loop, before the render step, rather than on the macrotask queue.
+- Rendering (style, layout, paint) occurs between macrotasks, not between microtasks. This is why a long microtask chain can freeze the UI.
+
+#### Where `requestAnimationFrame` and `requestIdleCallback` fit
+
+Within a single iteration of the event loop, the browser visits these phases in order:
+
+1. Run one task from a macrotask queue source.
+2. Drain the microtask queue (including any microtasks scheduled by step 1).
+3. If a render is due this turn, run all `requestAnimationFrame` callbacks queued for the next frame.
+4. Style, layout, and paint.
+5. During any remaining idle time before the next frame deadline, run `requestIdleCallback` callbacks.
+
+`requestAnimationFrame` schedules work for the next paint, making it the right tool for visual updates synchronized with the display refresh rate (~ 16.7 ms per frame at 60 Hz). `requestIdleCallback` schedules work for the period after rendering and only if the browser has idle time, making it suitable for non-urgent background work.
+
+```js live
+console.log('1: sync');
+queueMicrotask(() => console.log('2: microtask'));
+setTimeout(() => console.log('3: macrotask'), 0);
+requestAnimationFrame(() => console.log('4: rAF'));
+typeof requestIdleCallback === 'function' &&
+ requestIdleCallback(() => console.log('5: rIC'));
+console.log('6: sync');
+
+// Typical output: 1, 6, 2, 3, 4, 5
+// `5: rIC` may run later or be deferred under load
+```
+
+`setTimeout(fn, 0)` typically logs before the `requestAnimationFrame` callback because the timer's macrotask is dispatched on the next event loop turn, while `rAF` waits for the next paint (often a few milliseconds later at typical refresh rates). `requestIdleCallback` runs only after the browser finishes rendering, which is why it appears last and is the only callback in this example that may be deferred.
+
+### Node.js
+
+Built on libuv, with additional phases beyond what the HTML spec describes:
+
+- `process.nextTick()` has a higher-priority queue that drains before the promise microtask queue on every phase transition.
+- Macrotasks are divided into named phases: timers, pending callbacks, idle/prepare, poll (I/O), check (`setImmediate`), and close callbacks. Phases run in order, and microtasks together with `nextTick` drain between each.
+- At the top level of a script, the execution order of `setImmediate(fn)` and `setTimeout(fn, 0)` is not deterministic and depends on loop timing. Inside an I/O callback, `setImmediate` is guaranteed to run before `setTimeout(fn, 0)`.
+
+A comparison of the Node-specific scheduling primitives:
+
+| Mechanism | Queue | Runs at | Notes |
+| --- | --- | --- | --- |
+| `process.nextTick(fn)` | nextTick queue | Every phase transition, before the promise microtask queue | Highest priority — recursive use can starve I/O |
+| `queueMicrotask(fn)` / `Promise.then` | Microtask queue | Every phase transition, after the nextTick queue drains | Same semantics as in browsers |
+| `setImmediate(fn)` | Check phase | Once per loop iteration, after the poll (I/O) phase | Use to defer work until after the current I/O cycle |
+| `setTimeout(fn, 0)` | Timers phase | At the top of the next loop iteration once the delay elapses | Minimum delay clamped to 1 ms |
+
+Observed ordering at the top of a script:
+
+```js
+setImmediate(() => console.log('setImmediate'));
+setTimeout(() => console.log('setTimeout'), 0);
+Promise.resolve().then(() => console.log('promise'));
+process.nextTick(() => console.log('nextTick'));
+
+// Output:
+// nextTick
+// promise
+// setTimeout (or setImmediate — order between these two is not guaranteed at the top level)
+// setImmediate (or setTimeout)
+```
+
+### Web Workers
+
+- Each Worker has an independent event loop, with its own microtask and macrotask queues.
+- Messages posted via `postMessage` are enqueued as macrotasks on the receiving Worker's event loop.
+- No access to `requestAnimationFrame` or the DOM.
+
+## Common misconceptions
+
+Several statements about the event loop appear frequently in explanations and in responses generated by large language models but are inaccurate:
-1. `Start` and `End` are logged first because they are part of the initial script.
-1. `Promise 1` is logged next because promises are microtasks and microtasks are executed immediately after the items on the call stack.
-1. `Timeout 1` and `Timeout 2` are logged last because they are macrotasks and are processed after the microtasks.
+1. **"`setTimeout(fn, 0)` runs immediately after the current synchronous code."** Microtasks drain first. A `Promise.resolve().then(fn)` scheduled after a `setTimeout(fn, 0)` still runs before the timer callback.
+2. **"`await` blocks the event loop."** The `await` expression pauses the containing async function and returns control to the caller. The continuation is scheduled as a microtask and does not block other tasks.
+3. **"Microtasks run on a separate thread."** JavaScript execution is single-threaded. Microtasks run on the main thread, interleaved with macrotasks under the event loop's scheduling rules.
+4. **"`Promise.resolve()` is synchronous when the promise is already resolved."** The resolution is synchronous, but `.then` callbacks are always scheduled asynchronously as microtasks. This is a Promises/A+ requirement intended to guarantee consistent execution ordering.
+5. **"`process.nextTick` is a microtask."** In Node.js, `nextTick` has its own queue that drains before the promise microtask queue.
+6. **"`setTimeout(fn, 0)` fires after 0 milliseconds."** Both the HTML specification and Node.js clamp the minimum to a small non-zero value (4ms for nested timers in browsers; 1ms in Node). A delay of `0` is a lower bound, not a guarantee.
## Further reading and resources
-- [The event loop - MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop)
+- [The event loop — MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop)
+- [HTML Living Standard: Event loops](https://html.spec.whatwg.org/multipage/webappapis.html#event-loops)
- [The Node.js Event Loop](https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick)
- [Event loop: microtasks and macrotasks](https://javascript.info/event-loop)
-- [JavaScript Visualized: Event Loop by Lydia Hallie](https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif)
- ["JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue" by Lydia Hallie](https://www.lydiahallie.com/blog/event-loop)
- ["What the heck is the event loop anyway?" by Philip Roberts](https://2014.jsconf.eu/speakers/philip-roberts-what-the-heck-is-the-event-loop-anyway.html)
- ["In The Loop" by Jake Archibald](https://www.youtube.com/watch?v=cCOL7MC4Pl0)
diff --git a/questions/what-is-the-difference-between-double-equal-and-triple-equal/en-US.mdx b/questions/what-is-the-difference-between-double-equal-and-triple-equal/en-US.mdx
index a89998a..27e888f 100644
--- a/questions/what-is-the-difference-between-double-equal-and-triple-equal/en-US.mdx
+++ b/questions/what-is-the-difference-between-double-equal-and-triple-equal/en-US.mdx
@@ -4,88 +4,169 @@ title: What is the difference between `==` and `===` in JavaScript?
## TL;DR
-`==` is the abstract equality operator while `===` is the strict equality operator. The `==` operator will compare for equality after doing any necessary type conversions. The `===` operator will not do type conversion, so if two values are not the same type `===` will simply return `false`.
+`==` is the abstract equality operator while `===` is the strict equality operator. `==` performs type coercion before comparing, following the Abstract Equality Comparison algorithm defined in the ECMAScript specification. `===` does not perform coercion and returns `false` whenever the operand types differ. `===` is generally preferred in application code because it eliminates a class of bugs caused by unexpected coercion. The most common exception is `x == null`, which checks for both `null` and `undefined` in a single comparison.
| Operator | `==` | `===` |
| --- | --- | --- |
-| Name | (Loose) Equality operator | Strict equality operator |
-| Type coercion | Yes | No |
-| Compares value and type | No | Yes |
+| Name | Loose (abstract) equality operator | Strict equality operator |
+| Type coercion | Yes — per the Abstract Equality Comparison algorithm | No |
+| Comparison behavior | Types may be coerced before the value comparison | Types are compared first |
+
+> **Don't confuse `=` with `==` and `===`.** `=` is the [assignment operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Assignment) — it sets a variable's value (`x = 5`) and does not compare anything.
---
-### Equality operator (`==`)
+## The Abstract Equality Comparison algorithm
+
+The behavior of `==` is defined by the [`IsLooselyEqual`](https://tc39.es/ecma262/#sec-islooselyequal) algorithm in ECMA-262 §7.2.15. Given operands `x` and `y`, the algorithm proceeds as follows:
+
+1. If `Type(x)` is the same as `Type(y)`, return the result of `x === y` (strict equality, without coercion).
+2. If `x` is `null` and `y` is `undefined`, return `true`.
+3. If `x` is `undefined` and `y` is `null`, return `true`.
+4. If `Type(x)` is Number and `Type(y)` is String, return `x == ToNumber(y)`.
+5. If `Type(x)` is String and `Type(y)` is Number, return `ToNumber(x) == y`.
+6. If `Type(x)` is BigInt and `Type(y)` is String, convert `y` with `StringToBigInt`. Return `false` if the conversion is undefined; otherwise compare the resulting BigInts.
+7. If `Type(x)` is String and `Type(y)` is BigInt, swap operands and apply step 6.
+8. If `Type(x)` is Boolean, return `ToNumber(x) == y`.
+9. If `Type(y)` is Boolean, return `x == ToNumber(y)`.
+10. If `Type(x)` is String, Number, BigInt, or Symbol, and `Type(y)` is Object, return `x == ToPrimitive(y)`.
+11. If `Type(x)` is Object and `Type(y)` is String, Number, BigInt, or Symbol, return `ToPrimitive(x) == y`.
+12. If one operand is a BigInt and the other is a Number, return `true` if the mathematical values are equal; otherwise `false`.
+13. Return `false`.
+
+Four properties of the algorithm that are not apparent from a truth table alone:
+
+- Boolean operands are always converted to Number first (via step 8 or 9). This is why `true == '1'` is `true`: `true` becomes `1`, then step 5 converts `'1'` to `1`, producing `1 == 1`.
+- Object operands are reduced to primitives via `ToPrimitive` (steps 10 and 11), which invokes `Symbol.toPrimitive`, then `valueOf`, then `toString`. For example, `[1] == 1` is `true` because `[1].toString()` is `'1'`, which then coerces to `1`.
+- `null` and `undefined` are only loose-equal to each other and to themselves (steps 2 and 3). They are not coerced to `0` or `false` elsewhere in the algorithm, which is why `a == null` is a valid idiom for testing "null or undefined".
+- `NaN` is not equal to any value, including itself, under any equality operator. Use `Number.isNaN(x)` or `Object.is(x, NaN)` to test for it.
+
+### The coercion helpers used by `==`
+
+`==` dispatches to three type-conversion routines defined in ECMA-262 §7.1:
+
+- **[`ToPrimitive(input, hint)`](https://tc39.es/ecma262/#sec-toprimitive)** — returns the input unchanged if it is already a primitive. Otherwise invokes `input[Symbol.toPrimitive](hint)`, then falls back to `valueOf()` and `toString()`. If none returns a primitive, a `TypeError` is thrown.
+- **[`ToNumber(argument)`](https://tc39.es/ecma262/#sec-tonumber)** — `undefined` becomes `NaN`; `null` becomes `+0`; `true` and `false` become `1` and `+0`; strings are parsed with whitespace trimming, with the empty string becoming `0`; Symbols and BigInts throw `TypeError`; objects are first reduced via `ToPrimitive(argument, "number")` and the result is recursed on.
+- **[`ToString(argument)`](https://tc39.es/ecma262/#sec-tostring)** — `undefined` becomes `"undefined"`; `null` becomes `"null"`; Booleans become `"true"` and `"false"`; Numbers use `Number::toString`; Symbols throw `TypeError`; objects are first reduced via `ToPrimitive(argument, "string")`.
+
+## Examples
+
+The examples below apply the algorithm above to values that commonly produce unexpected results.
-The `==` operator checks for equality between two values but performs type coercion if the values are of different types. This means that JavaScript will attempt to convert the values to a common type before making the comparison.
+### Array compared with boolean
```js live
-console.log(42 == '42'); // true
-console.log(0 == false); // true
-console.log(null == undefined); // true
console.log([] == false); // true
-console.log('' == false); // true
+console.log([0] == false); // true
+console.log([1] == true); // true
+console.log([1, 2] == '1,2'); // true
```
-In these examples, JavaScript converts the operands to the same type before making the comparison. For example, `42 == '42'` is true because the string `'42'` is converted to the number `42` before comparison.
+Walking through `[] == false`:
+
+1. Step 9: `false` is coerced to `0`, producing `[] == 0`.
+2. Step 11: the array is coerced via `ToPrimitive`. `Array.prototype.toString` joins elements with commas, so `[].toString()` is `''`, producing `'' == 0`.
+3. Step 5: `'' == 0` coerces the string to `0`, producing `0 == 0`.
+4. Step 1: same types, strict equality returns `true`.
+
+The same process explains the remaining cases.
-However, when using `==`, unintuitive results can happen:
+### `[] == ![]`
```js live
-console.log(1 == [1]); // true
-console.log(0 == ''); // true
-console.log(0 == '0'); // true
-console.log('' == '0'); // false
+console.log([] == ![]); // true
```
-As a general rule of thumb, never use the `==` operator, except for convenience when comparing against `null` or `undefined`, where `a == null` will return `true` if `a` is `null` or `undefined`.
+Evaluation order:
+
+1. `![]` is evaluated first. Applying `ToBoolean` to any object yields `true`, so `![]` is `false`.
+2. The expression is now `[] == false`, which evaluates to `true` via the steps shown above.
+
+### Object compared with boolean
```js live
-var a = null;
-console.log(a == null); // true
-console.log(a == undefined); // true
+console.log({} == false); // false
```
-### Strict equality operator (`===`)
+Walking through:
+
+1. Step 9: `false` becomes `0`, producing `{} == 0`.
+2. Step 11: the plain object is coerced via `ToPrimitive`. `Object.prototype.toString` returns `'[object Object]'`.
+3. Step 5: `'[object Object]' == 0` calls `ToNumber('[object Object]')`, which produces `NaN`.
+4. Step 1 with `NaN == 0`: strict equality returns `false`.
+
+This differs from `[] == false` because the two objects have different `toString` results. This case is a frequent source of incorrect output in AI-generated explanations that treat all objects as equivalent to `[]` for coercion purposes.
-The `===` operator, also known as the strict equality operator, checks for equality between two values without performing type coercion. This means that both the value and the type must be the same for the comparison to return true.
+### `null` and `undefined`
```js live
-console.log(42 === '42'); // false
-console.log(0 === false); // false
-console.log(null === undefined); // false
-console.log([] === false); // false
-console.log('' === false); // false
+console.log(null == undefined); // true
+console.log(null == 0); // false
+console.log(null == false); // false
+console.log(null >= 0); // true
```
-For these comparisons, no type conversion is performed, so the statement returns `false` if the types are different. For instance, `42 === '42'` is `false` because the types (number and string) are different.
+- `null == undefined` is `true` by the special case in step 2.
+- `null == 0` is `false` because no step in `==` converts `null` to `0`.
+- `null == false` is `false` for the same reason — `false` becomes `0`, but `null` is not coerced, and step 13 returns `false`.
+- `null >= 0` is `true` because relational operators do not use the Abstract Equality algorithm. They apply `ToNumber` directly, converting `null` to `0`, producing `0 >= 0`.
+
+A consequence: the three expressions `null >= 0`, `null <= 0`, and `null != 0` are all `true` simultaneously.
+
+### Same-type string comparison does not coerce
```js live
-// Comparison with type coercion (==)
-console.log(42 == '42'); // true
-console.log(0 == false); // true
-console.log(null == undefined); // true
+console.log(0 == ''); // true
+console.log(0 == '0'); // true
+console.log('' == '0'); // false
+```
+
+`0 == ''` and `0 == '0'` both convert the string to a number (step 5). `'' == '0'` is a comparison between two strings; step 1 defers to strict equality, which compares the string contents directly. The strings `''` and `'0'` are not equal.
+
+A consequence: `==` is not transitive. `a == b` and `a == c` together do not imply `b == c`.
-// Strict comparison without type coercion (===)
-console.log(42 === '42'); // false
-console.log(0 === false); // false
-console.log(null === undefined); // false
+### `Symbol` equality
+
+```js live
+const s = Symbol('x');
+console.log(s == s); // true
+console.log(s == 'x'); // false
+console.log(s === s); // true
```
-### Bonus: `Object.is()`
+Two Symbols are loosely equal only if they are the same value (step 1). A Symbol compared with a string falls through the algorithm to step 13 and returns `false` without attempting any coercion that would throw.
-There's one final value-comparison operation within JavaScript: the [`Object.is()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) static method. The only difference between `Object.is()` and `===` is how they treat signed zeros and `NaN` values. The `===` operator (and the `==` operator) treats the number values `-0` and `+0` as equal, but treats `NaN` as not equal to each other.
+## Common misconceptions
-## Conclusion
+The following statements appear frequently in documentation, teaching material, and responses generated by large language models, but are incorrect:
+
+1. **`{} == false` is `true`: ** It is `false`. `{}` coerces to `'[object Object]'`, which coerces to `NaN`, which is not equal to `0`.
+2. **`[] == ![]` is `false`: ** It is `true`. `![]` is `false`, and `[] == false` follows the coercion steps to `true`.
+3. **`null == false` is `true` because `null` is falsy: ** It is `false`. The `==` algorithm has no step that coerces `null` to a boolean or number; `ToBoolean` is a separate operation used by conditional expressions, not by `==`.
+4. **`==` is transitive: ** It is not. `0 == ''` and `0 == '0'` are both `true`, but `'' == '0'` is `false`.
+5. **`NaN == NaN` is `true`: ** `NaN` is not equal to any value under `==`, `===`, or a relational comparison. Use `Number.isNaN(x)` or `Object.is(x, NaN)`.
-- Use `==` when you want to compare values with type coercion (and understand the implications of it). In practice, the only reasonable use case for the equality operator is to check for both `null` and `undefined` in a single comparison for convenience.
-- Use `===` when you want to ensure both the value and the type are the same, which is the safer and more predictable choice in most cases.
+## `Object.is()`
-### Notes
+[`Object.is(x, y)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) returns the same result as `===` with two exceptions:
+
+- `Object.is(NaN, NaN)` is `true`, whereas `NaN === NaN` is `false`.
+- `Object.is(+0, -0)` is `false`, whereas `+0 === -0` is `true`.
+
+There's one final value-comparison operation within JavaScript: the [`Object.is()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) static method. The only difference between `Object.is()` and `===` is how they treat signed zeros and `NaN` values. The `===` operator (and the `==` operator) treats the number values `-0` and `+0` as equal, but treats `NaN` as not equal to each other.
+
+## Conclusion
- Using `===` (strict equality) is generally recommended to avoid the pitfalls of type coercion, which can lead to unexpected behavior and bugs in your code. It makes the intent of your comparisons clearer and ensures that you are comparing both the value and the type.
-- ESLint's [`eqeqeq`](https://eslint.org/docs/latest/rules/eqeqeq) rule enforces the use of strict equality operators `===` and `!==` and even provides an option to always enforce strict equality except when comparing with the `null` literal.
+- Use `x == null` when a single check for `null` or `undefined` is required. ESLint's [`eqeqeq`](https://eslint.org/docs/latest/rules/eqeqeq) rule allows this pattern via the `{ "null": "ignore" }` option.
+- Use `Object.is` when `NaN` equality or distinguishing `+0` from `-0` is required.
+- When questioned about an unexpected `==` result, work through the algorithm steps rather than relying on memorized truth tables. The algorithm is short and fully specifies the behavior.
-### Further reading
+## Further reading
+- [ECMA-262 §7.2.15 `IsLooselyEqual`](https://tc39.es/ecma262/#sec-islooselyequal)
+- [ECMA-262 §7.1 Type conversion](https://tc39.es/ecma262/#sec-type-conversion)
- [Equality (==) | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Equality)
- [Strict equality (===) | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality)
+- [`Object.is()` | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)
+- [ESLint `eqeqeq` rule](https://eslint.org/docs/latest/rules/eqeqeq)