Everything the adaptive question bank can teach and test in JavaScript, from foundations through advanced practice. Work through it in order, or start practicing and let the questions find your level.
In an if or loop body the split shows: a var is visible throughout the enclosing function, while a let ends at the closing brace. let and const also refuse to redeclare a name in one scope, and at the top level of a non-module script a var becomes a property of the global object, but let and const do not.
Reading the binding early throws a ReferenceError, and so does applying typeof to it, even though typeof on a never-declared name quietly returns "undefined". The zone is about time, not position: a function written above the declaration may read the binding, as long as it is called after that line has run.
So push still works on a const array and new keys still land on a const object. A reassignment is caught only when its line runs, as a TypeError, so a stray count = 0 inside a rarely taken branch can hide for a long time. To make the object itself read-only, use Object.freeze, which covers only its top level.
So greet() works above function greet() {}, but above var greet = function () {} it throws TypeError: greet is not a function, because the name exists and holds undefined until the assignment runs. With const instead, the early call is a ReferenceError: the name is unusable until its declaration runs.
You get undefined by default: a missing argument, an absent property, a function with no return. null appears only where something sets it on purpose, as match does on a failed search. The typeof result dates to the first engine: objects carried type tag zero, and so did null, stored as a zero pointer.
The seven are string, number, bigint, boolean, symbol, undefined and null. The wrapper behind "hi".length is discarded at once, so a property you set on a primitive is gone on the next line, and strict mode throws instead. Avoid making wrappers yourself: new Boolean(false) is an object, and every object is truthy.
The full set is "undefined", "boolean", "number", "bigint", "string", "symbol", "function" and "object". Comparing against anything else, such as "array" or "null", is always false. Arrays report "object" too, so test them with Array.isArray and test null with x === null; a class reports "function".
Open the brace or parenthesis on the same line as return and the value comes back intact. The opposite trap is a missing semicolon. None is inserted before a line that begins with ( or [, so that line is read as a call or index on the previous expression, often failing with a confusing TypeError.
The directive counts only at the very start of a script or function body; after other statements it is an ignored string. Strict code also rejects with, duplicate parameter names and legacy octal literals such as 010 at parse time. A function with default, rest or destructured parameters may not opt in from its own body.
A typo is the usual culprit: totl = 5 leaves total unchanged and plants a new property on the global object, where it outlives the call. Strict mode turns that line into ReferenceError: totl is not defined. Before globalThis, portable code had to guess between window, self and global, names that depend on the host.
Values are pulled through the iteration protocol, the same mechanism for...of uses, so the pattern works on strings, Sets and generators but throws a TypeError on a plain object or null. When the swap sits on a line of its own, end the previous statement with a semicolon, or the leading [ joins the two lines.
To destructure into variables that already exist, wrap the statement in parentheses, as in ({ a, b } = obj);, because a statement starting with { is parsed as a block. Named keys are read like ordinary property access, inherited ones included, but a rest object copies only own enumerable properties, one level deep.
Defaults are evaluated lazily, left to right, so { a, b = a * 2 } can use a, and a costly call in a default runs only when its key is missing or undefined. Reading a binding declared later in the same pattern throws a ReferenceError once that default fires. To treat null as missing too, apply ?? afterward.
B. Operators, coercion and equality
Coercion makes == inconsistent: "" == 0 and 0 == "0" are both true, yet "" == "0" is false, and [] == false is true as well. Between two objects no conversion happens, so [1] == [1] is false, exactly as with ===. A safe habit is === everywhere except x == null, the one loose test that is easy to predict.
That makes x == null a single check for null or undefined, and it stays false for 0, "" and false. Relational operators play by other rules: null >= 0 is true because >= converts null to 0, while undefined >= 0 is false because undefined becomes NaN.
That makes it the right test for change detection: next !== prev reports a NaN that never moved as a new value on every pass, while Object.is(next, prev) sees no change. Otherwise it behaves like ===, with no coercion, so Object.is("1", 1) is false and two separate objects never match.
Loose equality runs only for == and !=. Strict equality also drives indexOf, lastIndexOf and switch, so case NaN: can never match. SameValueZero, which lets NaN equal itself, drives includes and the keys of Map and Set, and SameValue, which also tells -0 from 0, is what Object.is gives you.
Evaluation runs left to right, so 1 + 2 + "3" gives "33", but "1" + 2 + 3 gives "123". Objects count as well: arrays become strings first, so [1] + [2] produces "12". When numbers arrive as text, from parsed input for example, convert them with Number() before adding, or + will join them.
Only the hint "string", sent by template literals and String(), tries toString first; "number" from -, * and <, and "default" from + and ==, both try valueOf first. Date treats "default" as "string", so date + 1 appends text while date - 1 gives a number.
The trap is a guard such as if (count), which skips a legitimate 0 as though it were missing. Values that look false or empty can still pass: the string "false", an empty object and new Boolean(false) are all truthy. When 0 or an empty string is valid data, test for exactly what you mean, such as count !== undefined.
a || b gives the first truthy operand, or the last one if none is truthy, and a && b gives the first falsy operand, or the last. That is why port || 8080 works as a default and also why it discards a deliberate 0. Wrap the expression in Boolean() when a caller needs a real true or false.
Every other falsy value survives as well, including false and NaN, so NaN ?? 0 stays NaN and a failed calculation remains visible. Pick ?? if a zero or a blank string is a legitimate value, and || when any falsy value should trigger the fallback. Like ||, it skips the right side whenever the left side is kept.
a ||= b acts like a || (a = b), not a = a || b: when the left side already decides the result, nothing is written. So in strict mode a frozen property holding 0 survives ??= but makes ||= throw, and cache[key] ??= build() calls build only when the entry is null or undefined.
The guard is narrow: only null or undefined to the left of ?. stops evaluation, so fn?.() still throws when fn holds a number. Parentheses end the protected range, which is why (a?.b).c throws when a is null, and a chain can never be the target of =. Use it where absence is expected, not to hide bugs.
So 2 ** 3 ** 2 means 2 ** 9, which is 512, not 64. The ban on -2 ** 2 exists because languages disagree on whether it means 4 or -4, so the parser makes you write (-2) ** 2 or -(2 ** 2). Unlike Math.pow, the operator also accepts BigInt operands, as in 2n ** 64n.
The ban covers && too, and it is reported when the code is parsed, so the whole script or module fails to load even if that line would never run. Parenthesize to show which reading you mean: when a is 0, (a ?? b) || c falls through to c, while a ?? (b || c) keeps the 0.
The fraction is dropped toward zero, so | 0 matches Math.floor only for non-negative numbers: -1.5 | 0 is -1, not -2. Values beyond the 32-bit range wrap, so 2 ** 31 | 0 is -2147483648. >>> reads the result as unsigned: -1 >>> 0 gives 4294967295, whereas -1 >> 0 stays -1.
When both operands end up as strings, the number step is skipped and they are compared character code by character code, so "10" < "9" is true, and so is ["10"] < ["9"]. Anything compared with NaN is false in both directions, which makes undefined < 1 and undefined > 1 both false.
Its legitimate home is a for update clause such as i++, j--. Elsewhere it mostly hides bugs: return a, b returns only b, and because assignment binds tighter, x = 1, 2 stores 1. The commas between arguments, array elements and declarations are separators, not this operator, so f((1, 2)) passes one argument.
C. Control flow, errors and cleanup
A thrown string or plain object has no stack property, so nothing records where the failure began. In a catch, check before reading fields, as in err instanceof Error ? err.message : String(err), because after throw null, reading err.message throws a fresh TypeError inside your handler.
A SyntaxError in your own source is reported before anything runs, so no try in that script can catch it; at run time you meet one from parsers such as JSON.parse. A ReferenceError means a name could not be resolved, reading a property of undefined is a TypeError, while new Array(-1) raises a RangeError.
Pass the arguments through as super(message, options) so the message and any cause land on the instance, then assign this.name; skip that and String(err) still reads Error: …. A subclass lets a catch test err instanceof ValidationError instead of matching message text, which breaks as soon as the wording changes.
Wrap at each layer that can add context, as in throw new Error("Could not load settings", { cause: err }), and a handler can follow err.cause back to the root failure, whose own stack is untouched. The outer message does not mention the cause, so a log line that prints only err.message hides what really went wrong.
The return value is computed before finally runs, so reassigning that variable there does not change what the caller gets. A return or throw inside finally also replaces any exception already on its way out, and the original error vanishes. Keep finally for cleanup, free of return, throw and break.
The typical case is a probe whose failure has one meaning, such as trying JSON.parse and falling back to a default. The risk is breadth: a bare catch swallows every error from the try, including a TypeError caused by a typo. Keep the protected block down to the one call you expect to fail.
A realm is a separate global environment, such as an iframe or a Node vm context, and each has its own Error, so errors from another realm fail instanceof Error. Error.isError (ES2026) checks a marker the Error constructor sets, so an object merely built on Error.prototype passes instanceof but not this test.
Strict matching means case 1: never catches the string "1", a common miss when the value comes from parsed text. The cases also share a single block, so declaring let total in two of them is a SyntaxError; wrap each case body in braces. Stacking empty cases, as in case "a": case "e":, is the honest use of fallthrough.
The value needs a [Symbol.dispose]() method or must be null or undefined; anything else throws a TypeError at the declaration. If the block throws and so does a dispose method, you catch a SuppressedError with the cleanup failure in error and the block's in suppressed. await using awaits Symbol.asyncDispose.
D. Loops and iteration statements
Symbol keys never appear, and neither do methods defined in a class body, which are non-enumerable. The inherited half is the risk: if any code adds an enumerable property to Object.prototype, it shows up in every for...in over a plain object. Prefer Object.keys(obj) for own keys, or test each key with Object.hasOwn.
A plain object is not iterable, so for (const v of obj) throws a TypeError; loop over Object.entries(obj) with const [key, value] instead. A Map already yields [key, value] pairs. Leaving early through break or a thrown error calls the iterator's return method, which lets a generator run its finally cleanup.
String keys break arithmetic: i + 1 produces "01", not 1. The loop also visits any extra named property attached to the array and any enumerable helper a library added to Array.prototype, while it skips the holes in a sparse array. Use for...of instead, with arr.entries() when you need the index as well.
Only a declaration in the head gets this treatment: declare let i; above the loop, write for (i = 0; …), and all passes share one variable again, so functions made in the body see its final value. Each fresh copy starts from the previous pass's value before the update, so changing i in the body carries forward.
Its main use is a nested search: label the outer loop outer:, and break outer ends both loops once a match turns up. break may also target a labeled plain block, but continue must name a loop. Labels stop at function boundaries, so a callback cannot break an outer loop; an early return from a helper often reads better.
A return in the callback only ends that one call, acting like continue, and break there is a SyntaxError. forEach also ignores the promises an async callback returns, so every callback starts at once and the call returns before any of them finishes. To stop early or await each step, write a for...of loop.
Ten slow steps in the loop body therefore run one after another; to overlap independent work, start the promises first and await Promise.all(…). An array of promises that are already running is risky input. One that rejects before the loop reaches it counts as unhandled, and by default Node exits before the loop can catch it.
That fits work you must attempt before you can judge it, such as generating a candidate and then checking it, or reading a page of results before asking whether more remain. One quirk: continue inside do...while jumps to the condition, not back to the top of the body, so it can end the loop.
E. Functions and parameters
In let fact = function f(n) { … }, the name f exists only inside the function, so recursion through f keeps working even if fact is reassigned, while f outside is a ReferenceError. An anonymous expression still gets a name from the variable it is assigned to, and that is the name V8 prints in stack traces.
Because the default is rebuilt on every call, function add(item, list = []) never shares one array between calls. A later parameter may refer to an earlier one, as in (w, h = w), but not the reverse, which throws a ReferenceError when that default runs. Defaults also cannot see variables declared in the function body.
Inside an arrow, arguments silently refers to the enclosing function's arguments, not the arrow's own. In a sloppy-mode function with a plain parameter list, arguments[0] and the first parameter also alias each other, so writing one changes the other. Rest parameters never alias, and strict mode removes the link too.
f(...x) and [...x] require an iterable and fail on null or a plain object, while {...x} copies own enumerable properties from any value and treats null as empty. Spreading a huge array into a call such as Math.max can exceed an engine limit, which V8 reports as a RangeError; reach for a loop there.
Put the defaults inside the pattern, as in ({ size = 10 } = {}). Writing ({ size } = { size: 10 }) looks equivalent but helps only when no argument is passed: f({}) then sees size as undefined. The = {} covers a missing argument, not null, so f(null) still throws a TypeError.
So (a, b = 1, c) => {} reports 1: counting stops at the first default even though c follows. A destructured parameter counts once, and arguments plays no part. The neighboring name property records how a function was made, such as "bound greet" after bind or "get total" for a getter.
Since an arrow cannot construct, new on one throws TypeError: … is not a constructor, so anything callers might instantiate must be a regular function or a class. The missing bindings resolve outward: an arrow callback inside a class method can still call super.save(), and one in a constructor sees its new.target.
The wrapping parentheses make the parser read function as an expression; without them, function () {}() is a SyntaxError. Modules and block-scoped let now handle privacy, but the pattern survives as (async () => { … })(), a way to use await in a non-module script, where top-level await is not allowed.
In V8 the overflow reads RangeError: Maximum call stack size exceeded; Firefox throws an InternalError. Proper tail calls, which reuse the stack frame for a return f(x) call, have been required since ES2015, yet among major engines only Safari's JavaScriptCore performs them, in strict mode. Use a loop for unbounded depth.
The other half is mutation: user.name = "Ada" or list.push(x) inside the function changes the caller's object, since both names point at one value. Copy first, as with { ...user }, when the caller must not see the change. Pass by reference would also let a function rebind the caller's variable; JavaScript never does.
F. Binding, this and closures
The same function can get a different receiver, the object left of the dot, on every call: a.f() binds a, and after b.f = a.f, b.f() binds b. A regular function nested inside a method gets no this from it; as a forEach callback it sees undefined in strict code and the global object otherwise.
Only the function value travels, so the eventual call has nothing left of the dot. Class bodies are always strict, so a detached class method throws a TypeError the moment it reads a property of this. Pass () => obj.m() or obj.m.bind(obj) instead, and know that const { m } = obj detaches the method just as surely.
call and apply run the function at once and differ only in how arguments arrive: one by one, or as a single array-like. bind runs nothing; it returns a new function whose this no later call or second bind can change, although new still overrides it. An arrow function ignores the this passed to all three.
It reads this from the nearest enclosing regular function, as bound in the call that created the arrow, so arrows returned by make.call(a) and make.call(b) each keep their own receiver. A receiver supplied by an API is lost as well: [1].map(fn, ctx) hands ctx to a regular fn as this, but an arrow never sees it.
Returning a primitive such as 42 is ignored and the fresh object comes back anyway; only an object, array or function replaces it. Without new, a plain constructor function allocates nothing, so this.x = 1 throws in strict mode and quietly writes a global in sloppy mode, while a class refuses the call with a TypeError.
In sloppy code a detached this.count++ quietly creates a global count holding NaN; with "use strict", in an ES module or in a class body, it throws a TypeError. Sloppy mode also rewrites a passed this: f.call(null) becomes the global object and f.call(5) a Number wrapper, while strict code keeps null and 5.
Two closures made in the same call share one variable, so a write through either is visible to the other. Each call of the outer function creates a fresh variable, which is why closures from separate calls never interfere. To freeze a value, copy it into a new const or pass it as an argument when the closure is created.
In for (var i = 0; i < 3; i++), i lives in the surrounding function or script and outlives the loop, so callbacks that run afterward all read 3, the value that ended it, not 2. Before let, the fix was to pass i into an immediately invoked function; forEach gives the same per-call copy through its index parameter.
A factory like makeCounter() keeps count in a local variable that only its returned functions can touch, so no property access, Object.keys or JSON.stringify reveals it. The price is fresh copies of those functions on every call. Class #private fields match that privacy while instances share prototype methods.
Partial application returns one function that takes whatever remains, as add.bind(null, 1) does, while currying reshapes f(a, b, c) into the chain f(a)(b)(c). Curry helpers that read arity from fn.length misfire on a default or rest parameter: the count stops there, so the function may run before all arguments arrive.
G. Objects and properties
A missing id is a classic victim: cache[id] = v with id undefined stores under the string "undefined", which every later undefined lookup then finds. Numbers convert by their printed form, so obj[1.0] is obj["1"], and Object.keys hands back strings, which makes Object.keys(o).includes(1) false.
Without the brackets, { field: v } names a property literally called "field", the usual slip when the key sits in a variable. A computed key converts like obj[k]: a symbol stays a symbol, and { [1 + 1]: v } gets the key "2". Shorthand needs a binding named x in scope, or it throws ReferenceError.
Assignment and literals set all three flags to true, so the surprise comes from Object.defineProperty(o, 'x', { value: 1 }). That x is hidden from Object.keys and JSON.stringify, cannot be deleted, and refuses writes, throwing in strict code. A non-configurable property can never be made configurable again.
A setter that assigns to its own name, as set name(v) { this.name = v } does, calls itself until a RangeError stops it; keep the value under another key such as #name. Spread and JSON.stringify call the getter once and keep a plain value, and a getter with no setter ignores writes, or throws in strict mode.
On an empty dictionary, 'constructor' in counts is already true because {} inherits that name from Object.prototype, while Object.hasOwn(counts, 'constructor') is false. Use in when inherited members should count, as in a feature check, and hasOwn for data lookups. Both treat a key holding undefined as present.
Object.isFrozen(config) can report true while config.db.port = 1 still succeeds, because only the outer object's own properties were locked; a deep freeze must recurse. On a frozen array, push throws even in sloppy code, as all mutating array methods do, while a plain arr[0] = 9 there fails silently.
Object.preventExtensions blocks only new properties, so existing ones stay deletable and writable. Object.seal also makes every property non-configurable, so nothing can be deleted, though values stay writable; freeze then adds read-only. None can be undone, and a refused assignment or delete throws only in strict code.
After const copy = { ...state }, copy.user === state.user, so renaming the nested user changes both objects. Because only own properties travel, copying a class instance yields a plain object with none of its prototype methods, and { ...new Date() } is just {}. A deep copy has to recurse into every nested value.
The difference matters for a __proto__ key in parsed JSON: Object.assign({}, data) runs the inherited __proto__ setter and replaces the target's prototype, while { ...data } stores an ordinary own property by that name. Object.assign also throws when a write is refused, such as on a frozen target, even in sloppy code.
An object keyed by ids, such as { 30: a, 10: b }, lists 10 first whatever the insertion order, so a record of arrivals gets silently resorted; a Map keeps insertion order for every key. Only plain nonnegative integers move up: "01", "-1" and "1.5" stay in insertion order with the other strings.
The trip keeps only own enumerable string keys: symbol keys drop out, getters arrive as plain values, and a class instance comes back as a plain object without its prototype. Object.fromEntries accepts any iterable of pairs, so Object.fromEntries(map) converts a Map directly, and a repeated key keeps its last value.
After obj.x = undefined, both 'x' in obj and Object.keys still see the key; after delete obj.x they do not, and an inherited x from the prototype shows through again. On an array, delete arr[1] leaves length unchanged, so use splice when you mean to remove an element and close the gap.
In Object.assign({}, defaults, options) the caller's values win; swap the two and your defaults silently override the caller. A later source whose key holds undefined still overwrites, and a nested object replaces the earlier one whole instead of merging. Sources that are null or undefined are skipped without error.
H. Prototypes and inheritance
Each link is an object's hidden pointer to its parent, and falling off the end is not an error: the read yields undefined, so a misspelled method surfaces only when called, as TypeError: x.fnc is not a function. A plain {} stops at Object.prototype just before null, which is what supplies its toString.
Object.getPrototypeOf(new F()) is F.prototype, yet F's own link is Function.prototype, so the two answer different questions. Reassigning F.prototype later affects only objects built afterward, since each link is set when new runs. __proto__ is a legacy accessor for the link; prefer Object.getPrototypeOf.
Object.create(F.prototype) runs no constructor, so nothing initializes the result, even though instanceof F passes. The optional second argument takes descriptors, not values, with flags defaulting to false, so Object.create(p, { x: { value: 1 } }) yields an x that is read-only and hidden from Object.keys.
Assignment shadows, but mutation does not: child.items.push(x) first reads the inherited array, then changes the one every sibling shares. If the inherited property is non-writable, the write fails instead of making an own copy, and throws in strict code; only Object.defineProperty gets past it.
The setter runs with this bound to the instance being written, so it can store state there under a different key. An inherited getter with no setter goes further and refuses the write, throwing in strict mode. Object.defineProperty never consults setters, which makes it the way to force an own property past one.
It only checks whether F.prototype is on the chain, so Object.setPrototypeOf({}, F.prototype) passes without calling F, and replacing F.prototype fails old instances. A realm is a separate set of built-ins, such as an iframe's; an array from another realm fails instanceof Array, while Array.isArray works.
After F.prototype = { greet() {} }, an instance's constructor resolves through the new object to Object, so cloning with new x.constructor() builds the wrong type. Put constructor: F back in the replacement, or add methods to the existing object instead. Classes are immune: a class's prototype property is read-only.
With no toString or valueOf to call, ${dict} and String(dict) throw a TypeError, and dict.hasOwnProperty is not a function, so use Object.hasOwn. Object.groupBy (ES2024) returns this kind of object, which trips up code that calls methods on the result. JSON.stringify still works.
Since a.m === b.m, replacing Proto.prototype.m changes every existing instance at once, which is also why patching a built-in prototype reaches code you never wrote. A function assigned in the constructor, or an arrow held in a class field, is the opposite trade: a copy per instance, costing memory but tied to its object.
I. Classes
typeof C is still 'function', but the differences show: C() without new throws a TypeError, and its methods are non-enumerable and cannot be called with new. Inside the class body the name C is a constant binding, so reassigning it there throws, unlike a function declaration's own name.
Touching this too early is a ReferenceError, and so are reaching the end of the constructor without calling super() and calling it twice. Omit the constructor and the class gets one that forwards every argument to super(...args). The subclass's own fields are created the moment super() returns.
A field is an own enumerable property, so Object.keys and JSON.stringify see it, while a method is one non-enumerable function shared through the prototype. Fields are defined, not assigned, so an inherited setter of the same name never runs. In a derived class they appear when super() returns, after any code preceding it.
Being an own property, it outranks the prototype chain: a subclass that defines a handle() method still gets the parent's arrow from this.handle(), and super.handle() throws because the prototype has no such method. Use arrow fields for callbacks you hand out, and ordinary methods for anything a subclass should override.
Reading obj.#x on an object without the slot throws a TypeError rather than returning undefined, which is what the in check lets you avoid. The name exists only inside the class body that declares it, so a subclass that mentions it fails to parse, and a Proxy wrapped around an instance fails the same read.
Instances cannot see statics, so item.count is undefined where Item.count works. Subclasses inherit them because Object.getPrototypeOf(Child) === Item, and inside a static method this is whichever constructor received the call, so this.count++ run through Child.bump() writes a separate count onto Child.
Because the accessor is inherited rather than own, JSON.stringify(instance) and { ...instance } leave its value out; add toJSON when a computed getter must be serialized. Object.getOwnPropertyDescriptor(instance, 'f') returns undefined, so ask the prototype, and a class field of the same name hides the accessor.
map, filter and slice on an Array subclass build their result by calling your constructor with a length, so a constructor taking other arguments breaks them; return Array from static get [Symbol.species]() to opt out. The copying methods toSorted and with (ES2023) ignore species and return plain arrays.
On a plain call it is undefined; under new it names the constructor in the expression, even inside a parent constructor, so if (new.target === Shape) throw makes Shape abstract yet lets subclasses through. Arrows see the enclosing value, and if (!new.target) return new F(x) lets a function work with or without new.
The home object, the object a method was defined on, never changes, so lending a method that calls super.m() to an unrelated object still reaches the original parent's m. Only method syntax gets a home object: super works in { m() {} } inside an object literal, but in a function expression it is a SyntaxError.
Factories nest, as in class Doc extends Timestamped(Serializable(Base)); when two mixins define one method name, the outermost wins and can still reach the inner one through super. Each application builds a fresh class, so doc instanceof Timestamped is no test: it throws for an arrow factory and is false otherwise.
J. Numbers and arithmetic
The sum prints 0.30000000000000004, because one tenth has no finite binary expansion, while halves, quarters and whole numbers up to 2 ** 53 are stored exactly. Compare decimal results with a tolerance sized to their magnitude rather than ===, and keep money in integer cents so sums stay exact.
The limit is 2 ** 53 - 1; past it, neighboring integers collapse onto one value, so 2 ** 53 + 1 === 2 ** 53 is true. The usual victim is a 64-bit id read with JSON.parse, which arrives already rounded to a different number. Test with Number.isSafeInteger, and keep such ids as strings or BigInt.
One bad input poisons all that follows: any arithmetic touching NaN gives NaN, while typeof still reports "number". Sorting breaks too: a - b is NaN for any pair involving it, which sort reads as a tie, so in V8 [3, NaN, 1, 2].sort((a, b) => a - b) comes back unchanged. Screen values with Number.isNaN first.
The conversion cuts both ways: isNaN(undefined) is true, yet isNaN("") and isNaN(null) are false because both convert to 0, so it makes a poor validator for typed input. The same split separates the global isFinite("12"), which is true, from Number.isFinite("12"), which is false.
parseInt stops at the first unusable character, so "12px" gives 12 and "1e3" gives 1; Number reads those as NaN and 1000, yet turns "" into 0. Pass the radix because, without one, parseInt honors a 0x prefix: parseInt("0x1F") gives 31, while radix 10 stops at the x and gives 0.
It gives "1.00" because the literal 1.005 is stored as 1.00499999999999989…, just under the halfway point. Since the result is text, (0.1).toFixed(2) + 1 concatenates to "0.101"; convert back with Number() before more arithmetic, and format only once the math is done.
Rounding produces it quietly: Math.round(-0.4) and Math.ceil(-0.5) both return -0. String and JSON.stringify print "0", but toLocaleString() keeps the sign, which is how a rounded temperature ends up reading "-0°". Object.is(x, -0) detects it, and x + 0 turns it into +0.
Stepping backward with arr[(i - 1) % arr.length] therefore reads arr[-1], which is undefined, when i is 0. The divisor's sign never matters: 7 % -3 is 1. Odd checks break the same way, since -3 % 2 === 1 is false; test n % 2 !== 0 instead.
So Math.round(2.5) is 3 but Math.round(-2.5) is -2, which is neither half-away-from-zero nor banker's rounding (half to even). On -2.7, trunc gives -2 while floor gives -3. Never truncate with parseInt, which parses the string form: 0.0000005 prints as 5e-7, so parseInt(0.0000005) is 5.
Comparison is the exception: 1n < 2 and 1n == 1 are both true, though 1n === 1 is false. Division truncates, so 7n / 2n is 3n, and unary +, Math functions and JSON.stringify all throw on a BigInt. Convert on purpose with Number(b), which may round, or BigInt(n), which rejects fractions.
(-255).toString(16) is "-ff", a sign plus the magnitude; for the 32-bit pattern, apply >>> 0 first, so (-1 >>> 0).toString(16) is "ffffffff". Radixes outside 2 to 36 raise a RangeError. On a bare literal, 255.toString(16) is a SyntaxError because the dot is read as a decimal point; write (255).toString(16).
So Math.max(...scores) on an empty array quietly returns -Infinity; check the length first. One NaN argument makes the result NaN, while null counts as 0. Math.cbrt(-8) is -2 where (-8) ** (1/3) is NaN, and Math.hypot(1e200, 1e200) stays finite where squaring by hand overflows to Infinity.
Because nothing throws, a zero divisor travels on until something prints or serializes the result, and JSON.stringify writes Infinity, -Infinity and NaN all as null. Overflow produces Infinity too, as in 1e308 * 10. Number.isFinite rejects all three, and BigInt is the contrast: 1n / 0n throws a RangeError.
Whitespace counts as empty too, so a blank or all-space input string converts to 0 and slips past a naive numeric check; trim and test for an empty string before converting. Arithmetic follows the same rules, so null + 1 gives 1, undefined + 1 gives NaN, and even Number([]) is 0.
K. Strings and text
Because nothing changes in place, name.trim() on a line by itself has no effect: the trimmed copy is thrown away unless you assign it. Writing to an index such as s[0] = "J" is silently ignored in sloppy code and throws a TypeError in strict mode. Even s += "x" builds a new string and points the variable at it.
A code unit is a 16-bit slot; any code point above U+FFFF, the astral range, takes two of them as a surrogate pair, which slice can cut in half. Multi-code-point emoji go further: the man-woman-girl family emoji has a length of 8 and a [...s].length of 5, yet Intl.Segmenter counts it as one symbol, as a reader would.
Indexing the emoji "\u{1F600}" with [0] or charAt(0) returns only the first half of its surrogate pair, not a character by itself. codePointAt(i) reads a whole code point, yet i still counts code units, so codePointAt(1) there returns only the trailing half; Array.from(s) gives an array indexed by code point.
An accented letter such as é can be one precomposed code point or, in the decomposed "NFD" shape, a plain e plus a combining accent. Both render alike, yet ===, includes and Map keys see different strings, with lengths 1 and 2. Run normalize() on both sides first; it defaults to the composed "NFC" form.
The gap shows with computed indices: s.slice(2, -1) drops only the last character, while s.substring(2, -1) turns -1 into 0, swaps the pair and returns the first two characters. A reversed range gives an empty string with slice but a plausible wrong piece with substring, so slice is the safer default.
Given a plain string, replace always stops after the first occurrence and replaceAll changes every one, both matching the text literally, so . and + need no escaping. Given a regex, replace obeys the g flag, while replaceAll throws a TypeError if the flag is missing rather than stopping after one match.
Dollar sequences are interpreted, so raw user text can be corrupted: "$$5" comes out as $5, and any $& becomes the matched text. A function's return value goes in literally. The function receives the match, one argument per capture group, then the offset and the input, so each added group pushes the offset one place later.
A line break inside the backticks keeps the indentation of the next source line too, which is how multi-line messages pick up stray spaces. Each ${} asks an object for toString before valueOf, the opposite order from "" + obj on a plain object, and a Symbol value makes the literal throw a TypeError.
The chunks arrive as a frozen array that is always one longer than the values, so an expression at either end leaves an empty-string chunk there. The tag may return anything, which lets a query helper escape each value instead of pasting it in, and strings.raw keeps escapes such as \n unprocessed.
Plain < compares code-unit numbers, so "Z" < "a" is true and é lands after z. localeCompare follows a language's rules instead, putting "ä" before "z" in German but after it in Swedish, and { numeric: true } sorts "file9" before "file10". Check only the sign of its result, since nothing more is promised.
The fill defaults to a space, a multi-character fill is repeated and cut to fit, as in "5".padStart(4, "ab") giving "aba5", and an empty fill changes nothing. The method exists only on strings, so pad a numeric minute with String(n).padStart(2, "0"); calling it on the number itself throws a TypeError.
Splitting an empty string on a separator returns [""], one empty field rather than none. A limit argument discards the leftovers, so "a,b,c".split(",", 2) loses "c" instead of keeping "b,c" together. Going the other way, join() defaults to a comma and turns null and undefined elements into empty strings.
Besides spaces, all three remove tabs, line breaks, the no-break space and the byte-order mark. They leave a zero-width space, so text pasted from a web page can survive trim with an invisible character attached. To collapse runs inside the text, use replace(/\s+/g, " "); \s misses the zero-width space too.
s[-1] is not an error; it looks up a property literally named "-1" and returns undefined. Past either end, at also returns undefined while charAt returns an empty string. Because at counts UTF-16 code units, at(-1) on text ending in "\u{1F600}" yields only the trailing half of its surrogate pair.
Interpolating a missing value never fails, so null or undefined quietly appears as a word in your output; supply a fallback first with name ?? "". String(x) is also the safe choice for a Symbol, which + and a template literal both reject with a TypeError.
L. Arrays and indexed collections
Setting arr.length = 0 empties the array in place, so every other variable holding it sees the change, whereas arr = [] only repoints one name. Truncation cannot be undone, since raising length again brings back holes rather than the old values, and a negative, fractional or too-large length throws a RangeError.
A hole is an index with no property at all, which i in arr reports as false, unlike a stored undefined. So new Array(3).map((_, i) => i) returns three holes, since map never visits them; build ranges with Array.from({ length: 3 }, (_, i) => i), and note that find and includes treat holes as undefined.
Each element becomes a string first, so [10, 9, 1, 100].sort() gives [1, 10, 100, 9]. Pass (a, b) => a - b for numbers. Avoid a boolean comparator such as (a, b) => a > b, which never returns a negative number and so leaves the order to the engine: V8 hands [3, 1, 2] back unsorted.
The return value is the same object, so const sorted = list.sort() leaves sorted === list, and every other holder of list sees the new order. Stability has been required since ES2019, so sorting by a secondary key and then by the primary one keeps ties in the first order. toSorted() (ES2023) leaves the original alone.
splice returns the removed elements, not the edited array, while reverse and sort return the same array they changed. The copying versions (ES2023) return a new, shallow copy with holes filled by undefined, and with(i, v) rejects an out-of-range index with a RangeError rather than growing the array.
An arrow with braces and no return, such as x => { x * 2 }, gives map an array of undefined. filter keeps whatever its callback finds truthy, so filter(Boolean) drops 0 and "" along with null. Calling map just for side effects builds an array nobody reads, where a for...of loop says what you mean.
Without it the callback also starts at index 1, so items.reduce((sum, it) => sum + it.price) treats the first object as the sum and returns a string like "[object Object]5". A one-element array comes back as that element with no callback call; pass a start such as 0 whenever the result's type differs from the elements'.
A miss is undefined from find, [] from filter and -1 from indexOf. That -1 is truthy, so if (list.indexOf(x)) fails for a match at index 0 and passes for no match. find stops at the first hit while filter visits everything, and indexOf compares with ===, so searching by property needs findIndex.
SameValueZero is === with one change: NaN matches itself, while 0 and -0 still count as equal (Object.is is the comparison that tells those apart). Neither method looks inside objects, so [{ id: 1 }].includes({ id: 1 }) is false; to search by content, use some with a predicate.
Without an argument, flat goes one level deep, so [1, [2, [3]]].flat() still holds [3]; pass Infinity to flatten fully, and expect holes to vanish. A flatMap callback that returns [] removes its element and one that returns two items adds both, so a single pass can filter and expand at once.
A lone number passed to Array is a length, so Array(3) gives three empty slots and Array(2.5) a RangeError, while Array(3, 4) gives [3, 4]; Array.of always takes its arguments as elements. An array-like is any object with length and numbered keys, so Array.from({ length: 3 }, (_, i) => i) yields [0, 1, 2].
The result is [1, NaN, NaN]: radix 0 falls back to base 10, radix 1 is invalid, and "3" is not a binary digit. Any function with optional parameters is at risk when passed bare, since map supplies element, index and array. Wrap it, as in s => parseInt(s, 10), or use Number, which ignores extras but turns "" into 0.
at only reads, and arr[-1] = x is no way to write: it quietly adds a property named "-1" and leaves length alone. To replace the last element, assign to arr[arr.length - 1], or take a changed copy from arr.with(-1, x) (ES2023), which counts negative indices from the end the same way.
M. Keyed collections and weak references
A plain object stringifies non-symbol keys, so obj[1] and obj["1"] are one entry, objects used as keys collapse to "[object Object]", and obj["__proto__"] = v hits the prototype setter instead of storing anything. A Map avoids all three, but JSON.stringify turns it into {}, so plain objects still suit JSON records.
So map.set([1, 2], "a") followed by map.get([1, 2]) returns undefined, because each literal builds a new array. Keep the original reference, or derive a primitive key such as the string "1,2" when content should decide. Keys are compared with SameValueZero, so NaN works as a key and -0 is stored as 0.
Re-adding a member also leaves it in place, so order reflects first appearance and [...new Set(list)] dedupes while keeping the original order. Content does not count: two separate { id: 1 } objects are both kept, and passing a string to the constructor adds its characters, so new Set("hello").size is 4.
Each operation (ES2025) returns a new Set and leaves both inputs alone, while the subset predicates return booleans. The argument need only be set-like, exposing size, has and keys, which lets a Map work through its keys. An array, lacking size, throws a TypeError, so wrap it in new Set() first.
Write map.size with no parentheses, since map.size() throws a TypeError, while map.length quietly gives undefined. In V8 the read costs the same on a million-entry Map as on a tiny one. Object.keys(obj).length instead builds a fresh array of every key on each call, and still misses symbol keys.
Keys must be objects or unregistered symbols (ES2023); a string key throws a TypeError. An entry holds its value only while the key is reachable, even if the value points back at the key, which suits per-object caches. There is no size or iteration, since either would reveal when the engine reclaimed memory.
A target read through deref() survives until the current run of code finishes but may be gone by the next, so check for undefined every time. A FinalizationRegistry callback may run late or not at all, and never interrupts your code, so release files and connections explicitly and treat the registry as a backstop.
Both (ES2026) check whether the key is present, not what it holds, so an entry stored as undefined is returned, not replaced. getOrInsert(key, []) builds that empty array on every call, hit or miss, while getOrInsertComputed(key, k => []) runs its callback, with the key, only on a miss; WeakMap has both.
N. Iterators and generators
The same hook feeds spread, array destructuring, new Map() and Promise.all, so a plain object fails in each with a not-iterable TypeError, while Array.from quietly returns [] for it. Give a class a generator method named [Symbol.iterator] and every loop gets a fresh iterator, so the collection can be walked again.
When done is true, value holds the return value, which for...of and spread both discard, so a generator's return 5 never reaches the loop. An array's values() iterator is its own iterable, so a second spread of it gives []. Built-in iterators stay done, but a hand-written one does only if you code it that way.
Calling a generator function runs none of its body. The first line executes on the first next(), so an argument check at the top throws late, inside whatever loop consumes it. yield is valid only in the generator's own body, not in a nested callback such as one passed to forEach, and arrow functions cannot be generators.
yield inner() without the star hands out the generator object as one value, a common slip. yield* instead emits each of its values and passes next() arguments, throw() and return() through to it. Any iterable works, so yield* "ab" emits two letters, and yield* walk(child) makes a compact recursive tree walk.
The first call's argument is discarded, because no yield is waiting yet; that call only runs the body up to its first pause. A for...of loop always calls next() with no argument, so const cmd = yield state sees undefined there, and throw(err) resumes the generator by raising the error at the waiting yield instead.
A return or an exception leaving the loop closes the iterator too, as does destructuring only part of one, such as const [first] = gen(). A closed generator stays finished, so a second loop over it gets nothing. An array iterator has no return(), so breaking out of one and looping again resumes where it stopped.
An endless generator such as while (true) yield n++ is safe only if the consumer stops: spread, Array.from and rest destructuring try to collect it all and never return. Bound it with break or take(n) (ES2025); work happens as values are pulled, so a producer's error surfaces at the consumer's next().
They live on Iterator.prototype (ES2025), so built-in iterators and generators have them but arrays do not: write list.values().filter(f).take(10), and wrap a hand-built iterator with Iterator.from. Nothing runs until a consumer pulls, so take(10) on an endless generator does ten steps, and each chain is single-use.
Each next() returns a promise of { value, done }, and for await requests another only after its body finishes. Over an array of already-started promises it awaits them in order, so a later one that rejects first has no handler yet. Node ends the process for that by default, so use Promise.all there.
O. Promises and async/await
Later calls to resolve or reject are ignored without an error, so an executor that resolves and then reaches an error path still reports success. Resolved is not a synonym for fulfilled: a promise resolved with another promise follows it and stays pending until that one settles, or forever if it never does.
Because each then builds a fresh promise, p.then(a) followed by p.then(b) makes two branches that both get p's value, rather than a chain where b sees a's result. A handler's return value fulfills the new promise, a throw rejects it, and a returned promise is followed, so a promise of a promise never reaches you.
So wrapping a slow loop in new Promise does not make it asynchronous; the loop ends before the constructor returns. A throw inside the executor becomes a rejection only during that synchronous run, not from a callback it schedules. Even on a promise that has already settled, a then callback runs only once the current code is done.
A catch in mid-chain also recovers: whatever it returns fulfills the next promise, so the following then runs as if nothing failed, often with undefined. The same blind spot hits then(onOk, onErr), whose onErr never sees an error thrown by onOk; put one catch at the very end to cover every step.
The next then runs at once with undefined, racing the work it should wait for. If the orphan rejects, the chain's catch never sees it and the rejection goes unhandled. Braces usually cause it: x => { save(x) } returns nothing, x => save(x) returns the promise, and in async code the same bug is a missing await.
Promise.all yields values in input order or the first rejection reason, and rejecting cancels nothing: the other operations run on and their results are dropped. allSettled fulfills even when every input rejects, with one { status, value } or { status, reason } per input, so failures show only if you check status.
With race, a quick rejection wins just as a quick success would, which is what a timeout race relies on. any ignores rejections until all have failed, then lists every reason in the error's errors array. With an empty list, Promise.race([]) stays pending forever, while Promise.any([]) rejects at once.
A try around a call without await therefore catches nothing, and if (isReady()) always passes because a promise is a truthy object. Inside the function, return somePromise from a try block bypasses that block's own catch when the promise rejects, while return await somePromise routes the rejection through it.
Each await inside a for loop pauses before the next request starts, so three 50 ms calls take about 150 ms instead of about 50. forEach with an async callback fails the other way, returning before any call finishes; use await Promise.all(ids.map(load)), and keep the loop only when order or rate limits require it.
When a promise rejects with no handler, the language only notifies the host, which decides what follows. Node exits by default; browsers log it and fire an unhandledrejection event. Handlers added later in the same synchronous run or a microtask are in time, so attach yours before control returns to the event loop.
Promise.try(fn, ...args) (ES2025) calls fn at once, where Promise.resolve().then(fn) waits a microtask and Promise.resolve(fn()) lets a synchronous throw escape. Promise.withResolvers() (ES2024) returns { promise, resolve, reject }, so code outside an executor, such as an event listener, can settle it.
Ignoring a result does not stop the work: the loser of a Promise.race timeout runs to the end. AbortController comes from the host, not the language, and works only if the producer checks signal.aborted or listens for abort, then rejects with signal.reason, by default a DOMException named AbortError.
A thenable is any object with a then method. Returning one from an async function or a then handler calls that method too, so a class that defines then for other reasons runs it whenever awaited. If it never calls the resolve or reject function it receives, the await hangs with no error and no timeout.
P. The concurrency model and the event loop
The guarantee covers one synchronous stretch, not a whole async function: at each await, control returns to the caller, and other callbacks may run before the rest resumes. It also holds per thread only, so a worker writing to a SharedArrayBuffer can change memory between two of your reads.
Promise reactions and queueMicrotask callbacks are microtasks; timers, I/O completions and input events arrive as tasks, informally called macrotasks. A microtask queued during the drain joins that same drain, so a chain of already-resolved promises always finishes before the next timer callback, however long the chain.
A handler attached to an already-fulfilled promise still waits for the current synchronous code to finish, and handlers then run in the order they were registered. So a function that returns a cached value through Promise.resolve calls back only after the caller's remaining code, just as a slow lookup would.
The clamp is host policy rather than language: browsers raise delays under 4 ms to 4 once timer callbacks nest more than five levels deep, and Node turns any delay below 1 into 1 ms. Neither changes the ordering, since the callback is a task and cannot start until every queued microtask has run.
Splitting the loop with await does not help, because each continuation is a microtask that runs before any timer, input event or repaint gets a turn. Yield through a task instead, such as a zero-delay timer between chunks, or move the computation to a worker thread.
That holds even for await 42 or an already-fulfilled promise: the caller's remaining synchronous code runs first. So a condition you checked before the await, such as whether a cache entry exists, can be stale by the very next line.
It looks like a hang with no long loop in sight: timers never fire and, in a browser, the page stops repainting. A self-scheduling chain through then or queueMicrotask causes it, while the same recursion through setTimeout lets other tasks run between steps.
Neither is built into the language; both are small wrappers around host timers. Debounce suits a final value, such as searching once typing pauses, but never fires during a stream that never pauses; throttle suits steady sampling, such as scroll position, and can drop the last event unless it adds a trailing call.
Q. Modules
Named imports are checked when modules link, before any code runs, so a misspelled import { fromat } fails with a SyntaxError naming the missing export. A default import is checked only for existence: any local name is accepted, because default is an ordinary export with that name, as import { default as x } shows.
Every imported module finishes evaluating before your first line runs, so an import written at the bottom of a file still runs its side effects first, and its functions are callable at the top. An import inside an if or a function is a SyntaxError; for a condition or a computed path, use import().
CommonJS works differently: module.exports = { count } copies the current value, so a later increment never reaches code that called require. With ES modules the change shows through, but copying the import into a local const takes a snapshot that stops updating.
The binding is locked but the value is not: if the export is an object, any importer can change its properties, and every other importer sees the change. Only the exporting module can reassign the variable, typically through a function it exports, and the namespace object refuses both writes and new properties.
At the top of a module, this is undefined, a top-level var does not become a property of globalThis, and assigning to an undeclared name throws a ReferenceError instead of creating a global. To share a value, export it, or write to globalThis on purpose.
Hosts key that cache by resolved URL, not by the text you wrote: ./util.js and ../lib/util.js naming one file share an instance, while ./util.js?v=2 is a separate copy that runs again. A module whose top-level code threw stays failed, and every later import gets that same error.
The promise resolves to the namespace object, so a default export arrives as .default, and calling the result directly throws a TypeError. Unlike an import declaration, it also works in CommonJS files and classic scripts, and a missing file rejects the promise instead of halting the program.
export * never forwards default. When two star sources supply the same name, that name is silently dropped, and only code importing it explicitly fails, with a SyntaxError; a local export or an explicit export { x } from outranks any star export and settles the conflict.
If a.js loads first and imports b.js, which imports it back, b.js evaluates first; its top-level read of a const or class exported by a.js throws a ReferenceError, while calling an exported function declaration works. Move such reads into functions called later, or break the cycle.
The attribute (ES2025) is a check, not a conversion: the import fails unless the file really is JSON, so a compromised server cannot swap in code that runs. A JSON module has only a default export, and the older assert keyword from an earlier draft is a SyntaxError in Node 23.
R. Dates and times
No time zone is stored: getHours computes a local view on every call, so one Date shows different wall times on machines in different zones. The value is a whole number of milliseconds, with fractions truncated, limited to 100 million days on either side of 1970.
So new Date(2026, 1, 1) is February 1, and a month argument of 12 quietly becomes January of the next year. ISO strings count months from 1, which makes converting between "2026-02-01" and the numeric form a classic off-by-one, and getDay() also starts at 0, for Sunday.
West of Greenwich the date shows up a day early: in New York, new Date("2026-01-01").getDate() returns 31. For a calendar date, use new Date(2026, 0, 1), and give date-time strings an explicit Z or offset so no machine setting decides.
A helper that computes next week with d.setDate(d.getDate() + 7) also moves the caller's date, and const does not prevent it, since it only fixes the binding. Copy first with new Date(d); setters return the new millisecond count rather than the Date, so calls cannot be chained.
Month arithmetic suffers most: setMonth(1) on January 31, 2026 lands on March 3, because February 31 spills over. Day 0 means the last day of the previous month, so new Date(y, m + 1, 0).getDate() counts the days in month m, and a typo like February 30 builds a valid date.
Local getters answer for the machine's zone, so two servers can disagree about one instant's date, while the UTC getters agree everywhere. On a spring-forward day the local day lasts 23 hours, so adding 86400000 ms to midnight lands at 1:00, whereas setDate(getDate() + 1) keeps midnight.
Nothing throws at construction, so the NaN travels until something reads it: String(d) gives "Invalid Date", JSON.stringify writes null, and toISOString() throws a RangeError. Test with Number.isNaN(d.getTime()) right after parsing.
Temporal (stage 4) gives each kind of time its own type: PlainDate for a calendar date with no zone, ZonedDateTime for a wall time in a named zone, Instant for an exact moment. Months count from 1, and March 31 plus one month gives April 30, where Date rolls over to May 1.
S. Regular expressions
Use a literal for a fixed pattern and the constructor only for one assembled at run time, where a missed doubling fails silently: new RegExp("\d+") becomes /d+/, matching the letter d, and "\b" is a backspace, not a word boundary. A String.raw template keeps backslashes as typed.
The same /g regex given the same input twice returns true, then false, and filtering an array with a shared /g pattern silently drops items. match and replace reset lastIndex to 0 before running, but test, exec and matchAll begin wherever the previous call left it.
Their failures differ: with no match, match returns null rather than an empty array, so str.match(re)[1] throws, and matchAll throws a TypeError up front when the regex lacks g. Each matchAll result carries index and groups; spread the iterator into an array before you need a count.
The index and input properties vanish as well, so adding g to make a pattern find more silently breaks code that read a capture by position. When you need every match together with its groups, iterate matchAll instead.
Names survive edits: adding a group earlier in the pattern renumbers every index but leaves groups.year intact. The same name works in a backreference as \k<year> and in a replacement string as $<year>, and a group that took no part in the match reads as undefined.
On <a><b>, <.*> captures everything while <.*?> stops at <a>. Laziness does not prevent catastrophic backtracking: on a near-miss such as a run of a's followed by !, nested quantifiers like (a+)+$ try exponentially many splits, each extra character roughly doubling the time.
Without u, /^.$/ fails on an emoji because the emoji is stored as two 16-bit code units, and \p{L} quietly means the letter p followed by {L}. The v flag (ES2024) is stricter still: characters such as ( must be escaped inside a class, and it cannot be combined with u.
RegExp.escape (ES2025) writes a leading letter or digit as a \x escape, so the text cannot fuse with a preceding \1 into a different escape, and it writes a hyphen as \x2d because \- is a syntax error under the u flag. A hand-rolled helper that only inserts backslashes handles neither case.
Added in ES2025, these groups can scope only i, m and s; g, y, d, u and v describe the whole search, and writing them inside a group is a SyntaxError. One group can add and remove at once, as in (?i-s:…), but naming the same flag on both sides is also an error.
T. JSON and serialization
Symbol values follow the same rule, and at the top level all three make JSON.stringify return undefined rather than a string, which breaks code that expects text. Arrays write null so later elements keep their positions, while an object loses the key outright, so "key" in parsed turns false.
toJSON runs before a replacer function, so the replacer receives a Date already turned into a string and an instanceof Date check fails; read the original from this[key] instead. The method also receives its property key, and BigInt, which defines no toJSON, makes JSON.stringify throw.
An array list applies at every depth, so a nested key missing from it disappears too, though array elements are never filtered. A function is called first with an empty-string key and the whole value, then for each property on the way down, and returning undefined omits that property.
The final call gets an empty key and the whole result, and returning undefined deletes a property. JSON carries no type tags, so the reviver must recognize what to restore by key or pattern; a third context argument (ES2026) supplies a number's original text, rescuing integers past Number.MAX_SAFE_INTEGER.
A repeated but non-circular reference is fine, yet it is written out once per occurrence, so after parsing the two places hold separate objects. A replacer that skips objects it has already seen, tracked in a WeakSet, breaks cycles but also drops the second copy of a merely shared reference.
They fail in different ways: a Map or Set becomes {} silently, a BigInt throws, a Date comes back as a string, and an instance of your own class arrives stripped of its prototype and every method on it. The host function structuredClone keeps maps, sets, dates and big integers, though it still drops the class.
U. Symbols and metaprogramming
Skipped is not hidden: Reflect.ownKeys lists them, and spread and Object.assign copy them along, so a symbol prevents name collisions rather than access, which is the job of #private fields. Putting a symbol in a template literal throws a TypeError; use String(sym) or sym.description.
The registry is shared by every realm in the program, each frame's window included, so Symbol.for("app") returns the same symbol in all of them. Symbol.keyFor returns the key only for registry symbols, and a registered symbol cannot be a WeakMap key, though other symbols can (ES2023).
That makes toString-based type checks spoofable: an ordinary object tagged "Array" reports [object Array] while Array.isArray still says false. Built-ins such as Map and Promise set the tag themselves, and a class can define it as a getter to change how String(instance) reads.
Each trap matches one internal operation, not one piece of syntax: in fires has, but Object.hasOwn asks getOwnPropertyDescriptor, so a proxy that hides keys must cover both. In strict mode, a set trap that returns false turns the assignment into a TypeError.
On a frozen target, a get trap returning a different value throws on access, and has cannot report a non-configurable property as missing. Configurable properties carry no such check, so a proxy can disguise an ordinary object freely but cannot lie about a frozen one.
Each of the 13 Reflect functions matches a trap by name and arguments, so a get trap can end with return Reflect.get(...arguments). They report failure instead of throwing: Reflect.defineProperty on a frozen object returns false where Object.defineProperty throws a TypeError.
Read target[key] instead and a getter runs with this set to the target, so its own property reads skip your traps, and an object inheriting from the proxy sees the target's values in place of its own. Classes with #private fields are the exception: their getters fail when this is the proxy.
Proxy.revocable returns the proxy plus a revoke function; afterward every operation on the proxy throws a TypeError, though typeof still works. Objects already read through it stay usable, so withdrawing access to a whole object graph means wrapping each returned object in its own revocable proxy.
Direct eval can read the caller's local variables, while indirect (0, eval) and new Function see only global names, so neither of the latter can reach a closure's secrets. In browsers, a Content Security Policy without 'unsafe-eval' blocks all three, and JSON.parse is the safe way to read data.
V. Typed arrays and binary data
Indexing the buffer itself fails quietly: buf[0] is undefined, and buf[0] = 1 only adds an ordinary property while every byte stays zero. Its size is fixed at creation unless you pass maxByteLength, which makes a resizable buffer (ES2024) that resize() can grow or shrink.
Writing 1 through a Uint32Array shows up as bytes 1,0,0,0 in a Uint8Array over the same buffer on little-endian hardware, the usual case. subarray makes another view of that memory, while slice and new Uint8Array(otherView) copy, so choose by whether edits should propagate.
Uint8ClampedArray also rounds halves to even, storing 4.5 as 4, where Uint8Array truncates 1.7 to 1. Use the clamped type for color channels, where overflow should stick at full intensity, and remember that map on a plain Uint8Array returns another one, so a computed 300 wraps to 44.
Omit the flag and DataView uses big-endian, while typed arrays use the machine's order, little-endian on most hardware, so a value written by setUint16(0, 0x1234) reads back as 0x3412 through a Uint16Array. DataView also reads at any byte offset, where a typed-array view must start on a multiple of its element size.
Writes to index 5 of a two-element array, to -1, or to 1.5 are all discarded without an error, even in strict mode, while assigning to length there throws. To grow, allocate a larger array and copy the old one in with set(), or use a length-tracking view over a resizable buffer (ES2024).
The old route used the host's btoa and atob, which accept only characters with codes up to 255, so bytes had to be spread into a string one character at a time. The new methods (ES2026) work on bytes directly, accept { alphabet: "base64url" } for the URL-safe variant, and throw a SyntaxError on malformed input.
With only 10 stored fraction bits, a Float16Array (ES2025) keeps integers exact only up to 2048, so writing 2049 stores 2048, and a large value such as 70000 becomes Infinity. Math.f16round returns what a write would store, so you can check a value before committing it.
W. Internationalization
India groups digits as 12,34,567, yen amounts get no decimals, and French output separates thousands with a narrow no-break space, so a test comparing against a typed space fails. toFixed does none of this: it always writes a period and never groups digits.
The same instant can land on a different calendar date, not just a different hour: 15:30 UTC on October 4 is already October 5 in Asia/Tokyo. A misspelled zone name throws a RangeError, and formatToParts returns labeled pieces when you need a custom layout instead of string slicing.
Plain sort() compares UTF-16 code units, so "Zebra" lands before "apple" and accented words after z. A collator fixes that, { numeric: true } puts item2 before item10, and the order itself depends on the language: Swedish files ä after z, German beside a.
Intl.PluralRules answers with a category such as "one", "few" or "many" rather than a word, and you map each category to your own string; Polish needs three for whole numbers, Arabic six in all. With { numeric: "auto" }, RelativeTimeFormat says "yesterday" for -1 day, but computing the number and unit is still your job.
Unsupported requests fall back without an error: ["xx-XX", "fr-CA"] resolves to fr-CA, es-ZZ trims to es, and an unknown language lands on the runtime's default. Read resolvedOptions().locale to see what you got; a malformed tag such as en_US, by contrast, throws a RangeError.
X. Language evolution and version gates
Between 2 and 3 sits stage 2.7, where the design is frozen while tests are written. Even stage 3 can change: import assertions shipped in Chrome and Node with an assert keyword, went back to stage 2 in 2023, and returned as with, so Node 23 now rejects the assert form outright.
So typeof null still returns "object": a fix was proposed and rejected. New features bend around old code instead: Array.prototype.flatten shipped as flat after an early implementation broke sites built on the MooTools library, and globalThis has its name because global and self risked breaking existing scripts.
Parsing happens before any code runs, so new syntax cannot be guarded by an if in the same file: one unsupported token stops the whole script. Probe syntax by compiling a string inside try or loading a separate file, and probe built-ins directly, as in typeof Object.groupBy === "function" (ES2024).
Babel, for example, rewrites user?.name into older syntax but leaves Object.groupBy(...) (ES2024) untouched, so that call still fails on an old runtime unless a polyfill defines the method first. A few features resist both approaches: no older syntax can reproduce what a Proxy intercepts.
Only substr and escape live in Annex B, which browsers must support and other hosts may omit; with sits in the main specification, marked legacy and banned in strict code, including every module. Use slice, whose second argument is an end index rather than a length, and encodeURIComponent.
ECMAScript says how modules link and evaluate but leaves finding and fetching them to the host, so Node looks up a bare import "lodash" in node_modules while a browser needs an import map for it. A fresh engine context, such as one from Node's vm module, has Promise and JSON but no setTimeout.
Y. Explicit resource management — await using and the disposal stack
Drop the inner await and the declaration receives the pending promise itself, which has no disposer method, so await using (stage 4) throws a TypeError on that line. The outer await does nothing at the declaration; its pause comes only when the scope ends and the disposer runs.
Because the lookup in await using (stage 4) happens on that line, the method found there is the one that runs: reassigning obj[Symbol.asyncDispose] later in the block changes nothing at exit. The early failure works in your favor, since an object that cannot be cleaned up is rejected before any statement that uses it.
The wrapper calls the method and resolves without inspecting its result, so code after the block runs while the close is still pending, and if that promise rejects, no enclosing catch sees it. Keep Symbol.dispose synchronous and give asynchronous cleanup a Symbol.asyncDispose method (stage 4).
Under await using (stage 4), async disposers run one at a time: each returned promise settles before the next disposer starts, so one slow close delays the teardown of everything declared earlier. Declare a dependent resource, such as a transaction, after the connection it uses, and it closes first.
Several failing disposers produce a chain, so the error that started the exit can sit several .suppressed links deep; log by walking that chain rather than reporting only e.error. A disposal failure with no earlier error is thrown as itself, unwrapped, and a SuppressedError (stage 4) never carries a cause.
Only the nullish values qualify: cond && open() yields false when the condition fails, and false throws a TypeError, so write cond ? open() : null. The forced await also means the function's promise resolves a microtask later than with plain using (stage 4), even when nothing was acquired.
AsyncDisposableStack (stage 4) handles the factory case through move(): register each acquisition on a stack bound with await using, so a failure partway disposes what was already opened, then call move() on success to hand everything over intact. adopt() attaches cleanup to a value lacking a disposer.
Node 23.11 shows the gap: both symbols exist, and its timers and file handles carry disposers, yet using (stage 4) is a SyntaxError and DisposableStack is undefined. Test the syntax itself by compiling a snippet such as new Function("using x = null") inside try/catch.
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.