JavaScript
29 questions | entry to principal | answers written to be said out loud, not read
entry
What's the difference between var, let, and const, and what's the temporal dead zone?
I reach for const by default because it signals the binding won't be reassigned, and I use let when I know a value needs to change. var is function-scoped and gets hoisted and initialized to undefined. let and const are block-scoped and they're hoisted too, but they sit in the temporal dead zone until their declaration actually runs, so touching them earlier throws a reference error instead of silently giving you undefined.
What is destructuring and where do you use it day to day?
Destructuring lets me pull values out of arrays or objects straight into named variables instead of accessing them one property at a time. I use it constantly for function parameters, for pulling specific fields off an API response, and for swapping variables without a temporary one. I also lean on default values and renaming inside the same expression when the source shape doesn't match what I want to call things.
Walk me through default parameters, rest, and spread.
Default parameters let a function fall back to a value when an argument is missing or undefined, so I don't need manual checks inside the body. Rest collects the remaining arguments into a real array, which replaced the old arguments object for me. Spread does the opposite, it expands an array or object out into individual elements or properties, and I use it constantly for shallow copies and merging.
What do template literals give you over regular string concatenation?
Template literals let me embed expressions directly inside a string with the dollar-brace syntax, so I'm not chaining a bunch of plus signs together. They also support real multi-line strings without escape characters, which is a small thing but it makes generated HTML or SQL far more readable. On top of that they're the foundation for tagged templates, where a function gets to process the literal before it becomes a string.
Walk me through map, filter, and reduce.
map transforms every element and returns a new array of the same length. filter keeps only the elements that pass a test and returns a new, possibly shorter array. reduce is the general-purpose one, it walks the array with an accumulator and can build up anything, a sum, an object, a grouped structure, or even another array, so I reach for it whenever map and filter alone can't express what I need.
What is a Promise and what states can it be in?
A Promise represents a value that isn't available yet but will resolve or fail at some point in the future. It starts pending, and it settles exactly once into either fulfilled with a value or rejected with a reason, it can never go back to pending or flip from one settled state to the other. I think of a Promise as a container I can attach then and catch handlers to instead of passing callbacks around by hand.
junior
Can you explain closures and give a practical use for one?
A closure is what happens when a function remembers the variables from the scope it was defined in, even after that outer function has finished running. I use them all the time for things like a private counter, memoization caches, or debounce and throttle utilities where I need state that persists between calls but isn't exposed globally. The inner function just keeps a live reference to those outer variables instead of copying their values.
How do arrow functions handle this differently from regular functions?
An arrow function doesn't get its own this, it just inherits this from the surrounding lexical scope where it was written. A regular function's this depends on how it's called, which is what causes bugs inside callbacks and event handlers. That's why I reach for arrow functions for things like array callbacks or class methods where I want this to stay tied to the enclosing context.
== versus ===, and what's actually happening with type coercion?
=== compares value and type with no conversion, which is why I default to it everywhere. == first coerces one or both operands to a common type before comparing, and those coercion rules are full of surprising edge cases like empty string equaling zero. I only reach for == in the one case I actually want, checking a value against both null and undefined at once.
How does inheritance work with class and extends?
extends sets up the prototype chain so a subclass inherits methods from its parent, and inside the constructor I call super with the right arguments before I can use this. I use inheritance sparingly though, because composing smaller behaviors together usually ages better than a deep class hierarchy. It's most useful when there's a genuine is-a relationship and shared behavior that really belongs in one place.
What are getters and setters used for on a class or object?
A getter lets me expose a computed property that reads like a plain field instead of a method call, and a setter lets me intercept an assignment to validate or transform the value going in. I use them when I want the external API to stay simple while still controlling access underneath, like deriving a fullName from firstName and lastName. I try not to overuse them though, since hidden logic behind a plain-looking property can surprise someone reading the code.
What problem does the Symbol type solve?
Symbol gives me a value that's guaranteed unique, so it's mainly used to create object keys that won't collide with a string property someone else adds later. The language itself uses well-known symbols like Symbol.iterator to hook into behavior such as making an object usable in a for-of loop. I don't reach for Symbol often in application code, but it's the mechanism behind a lot of protocol-based features.
How do optional chaining, nullish coalescing, and logical assignment work together in practice?
Optional chaining with the question-mark-dot short circuits to undefined the moment it hits a null or undefined value, so I stop writing long chains of manual guard checks before reaching into nested data. Nullish coalescing then lets me supply a fallback, but only when the value is actually null or undefined, unlike the OR operator which also overrides a valid zero or empty string. Logical assignment operators like question-question-equals or or-or-equals combine that same check with an assignment, so I can lazily initialize a property in one line instead of writing an if block.
What do flat, at, includes, and findLast give you that the older array methods didn't?
flat flattens nested arrays to a given depth without me writing a recursive helper. at lets me index from the end with a negative number, so at of negative one replaces that awkward length-minus-one pattern. includes checks for a value directly and correctly handles NaN, which indexOf never did. findLast and findLastIndex search from the end forward, which saves me from reversing a copy of the array just to find the most recent match.
What are Object.entries and Object.fromEntries used for?
Object.entries turns an object into an array of key-value pairs, which is what lets me run array methods like map or filter directly over an object's data instead of writing a for-in loop. Object.fromEntries does the reverse, it takes that same kind of pairs array and rebuilds a plain object. I use the combination a lot for things like filtering out empty fields from a form object or transforming query parameters.
Why does immutability matter here, and what does Object.freeze actually guarantee?
I avoid mutating existing objects and arrays in place because it makes state changes predictable and keeps old references intact for comparisons, undo behavior, or a framework that relies on reference checks. Object.freeze stops new properties from being added and existing ones from being reassigned or deleted, but it's shallow, so a nested object inside a frozen object is still fully mutable. If I need real deep immutability I either freeze recursively or reach for an immutable data structure library.
How does async/await relate to Promises, and where do people get it wrong?
async/await is syntax on top of Promises, an async function always returns a Promise and await just pauses that function until the awaited Promise settles. The mistake I see most is awaiting things sequentially inside a loop when the calls don't actually depend on each other, which turns something that could run concurrently into a chain of round trips. If the work is independent I kick everything off first and await a Promise.all instead.
What problem does globalThis solve?
Before globalThis, getting a reliable reference to the global object meant checking for window in a browser, global in Node, or self in a worker, and picking the wrong one broke your code in that environment. globalThis is a single standardized name that always points to the global object no matter where the code is running. I mostly run into it when writing environment-agnostic library code or polyfills rather than everyday application code.
senior
What are tagged template literals and when have you actually needed one?
A tagged template is a function called with a template literal, and it receives the array of string pieces plus the interpolated values separately instead of one merged string. That separation is exactly what lets a library like a styled-components or a SQL client safely escape or process each value before assembling the final output. I don't write my own tags often, but understanding the mechanism explains a lot of syntax that otherwise looks like magic.
Are ES6 classes real classes, or just syntax over prototypes?
Under the hood a class is syntax over the same prototype chain JavaScript always had, there's no separate class system being added to the language. Methods I define on a class still end up on the prototype and get shared across instances instead of copied onto each one. Knowing that helps me reason about things like method lookup, instanceof checks, and why changing a prototype method affects every existing instance.
Explain iterators and generators, and how they relate.
An iterator is any object with a next method that returns a value and a done flag, and that's the protocol for-of and spread rely on. A generator function is just a much easier way to write one, because calling it returns an iterator automatically and I can pause execution with yield instead of hand-managing state between calls. I've used generators for things like lazily walking a large or infinite sequence without building the whole thing in memory first.
When do you reach for Map and Set instead of a plain object or array, and what's a WeakMap for?
Map lets me use any value as a key, keeps insertion order, and gives me a real size property, so I prefer it over a plain object whenever keys aren't just simple strings. Set is the same idea for a collection of unique values, and it saves me from manually deduping an array. WeakMap holds its keys weakly, so entries can be garbage collected once nothing else references the key, which makes it a good fit for attaching private metadata to an object without causing a memory leak.
structuredClone versus a shallow copy, when does the difference actually bite you?
A shallow copy, whether it's spread or Object.assign, only copies the top level, so any nested object or array inside is still shared by reference with the original. That's fine until someone mutates a nested field and it silently changes both copies. structuredClone gives me a true deep copy of most data types without needing a library, though it can't clone things like functions or DOM nodes, so I still reach for a targeted deep copy when I only need to duplicate one nested piece.
Promise.all versus allSettled versus race versus any, when do you use each?
Promise.all runs everything concurrently and resolves with all the values, but it rejects immediately the moment any single promise rejects, so I use it when every result is required. allSettled waits for every promise to finish regardless of outcome and gives me a status for each one, which is what I use when partial failure is acceptable and I still want the successes. race resolves or rejects as soon as the first promise settles at all, which is useful for something like a timeout. any resolves as soon as the first one succeeds and only rejects if every single one fails, which is the one I reach for when I have several equivalent sources and just want the fastest success.
How do you handle errors in async code?
Inside an async function I wrap the awaited call in a try/catch so a rejection turns into a normal catch block instead of an unhandled rejection. For a chain of Promises I put a single catch at the end rather than one after every then. I also try to throw or reject with real Error objects, or a custom error subclass when I need extra context, so whatever catches it upstream can tell failures apart instead of pattern-matching on a string message.
Walk me through the event loop and where microtasks fit in.
Synchronous code runs first on the call stack, and it has to finish completely before the event loop looks at anything else. Once the stack is empty, the microtask queue drains entirely, that's where Promise callbacks and queueMicrotask live, before the loop even considers the next macrotask like a setTimeout callback or an I/O event. That ordering is exactly why a resolved Promise's then callback always runs before a setTimeout of zero, even though both look like they should fire immediately.
What is dynamic import and why would you use it over a static import?
A static import has to be at the top of the file and gets resolved before anything runs, but import called as a function returns a Promise and can happen anywhere, including conditionally at runtime. I use it for code splitting, loading a heavy component, chart library, or admin-only feature only when it's actually needed instead of putting it in the initial bundle. It's also useful for lazily loading a module based on something you only know at runtime, like a locale or a feature flag.
staff
ES modules versus CommonJS, what actually differs?
CommonJS uses require and module.exports, it resolves synchronously, and it's the format Node used by default for years. ES modules use import and export, they're statically analyzable, which is exactly what lets a bundler do tree-shaking and what lets the engine load them asynchronously. The imports in an ES module are also live bindings rather than copied values, so if the exporting module updates a variable, an importer sees the updated value.
What is tree-shaking and what does it actually require to work?
Tree-shaking is a bundler removing exported code that nothing in the final bundle actually imports, so the shipped file only contains what's used. It depends on ES module syntax specifically, because static import and export let the bundler analyze the dependency graph ahead of time, which isn't possible with CommonJS's dynamic require calls. It also breaks down if a module has side effects the bundler can't prove are safe to drop, which is why libraries mark themselves side-effect-free in package.json to help the bundler out.
Fast recall
TDZ = temporal dead zone | closure = remembers outer scope | spread = expands into elements | rest = collects into array | Map = keyed collection, any key type | Set = unique value collection | WeakMap = garbage-collectable keyed cache | generator = pausable function with yield | microtask = runs before next macrotask | hoisting = declarations lifted up | tree-shaking = removes unused exports | structuredClone = deep copy built in | Promise.any = first success wins | globalThis = universal global reference