Most explanations of these concepts stop at the definition. Scope gets a rule about hoisting. Closures get a counter function that increments a number nobody would ever ship. The event loop gets a diagram. any versus unknown gets a one-line rule. Generics get a toy example with numbers and strings that never shows up in a real codebase.
That's enough to answer a question in an interview. It's not enough to actually recognize these patterns when they show up in your own code, which is the thing that matters once you're past the interview and actually shipping something.
This is the first in a short series working through concepts that come up constantly in interviews, explained the way I'd actually explain them to someone on my team, with real examples from production code, not just the textbook version.
var, let, and const
var, let, and const are the three ways to declare a variable in JavaScript. var is function-scoped and can be redeclared and reassigned freely. let is block-scoped and can be reassigned but not redeclared in the same scope. const is block-scoped and can't be reassigned after its initial value is set, though an object or array it points to can still be mutated internally.
The rule everyone learns is "use const by default, let when a value changes, avoid var." The part that's harder to see from that rule alone is what it looks like once real state is involved, not just a single reassignment.
Take pagination on a leads or submissions list, the kind of thing Formgrid.dev's dashboard actually does:
const pageSize = 20;
let currentPage = 1;
function nextPage() {
currentPage++;
}
pageSize never changes for the life of that list, so const says so directly. currentPage changes every time someone clicks through the pagination, so let is the honest choice. The value of picking correctly here isn't stylistic; it's that anyone reading this code later can tell, without tracing every line, which values are safe to assume are fixed and which ones move. var doesn't offer that signal at all, and it doesn't respect block scope the way let and const do, which is reason enough to leave it out of new code entirely.
Closures
A closure is a function that keeps access to variables from the scope it was created in, even after that outer scope has already finished running.
The clearest real-world use of a closure is a small API client factory:
function createApiClient(baseUrl: string) {
return {
get(endpoint: string) {
return fetch(`${baseUrl}${endpoint}`);
},
post(endpoint: string, data: unknown) {
return fetch(`${baseUrl}${endpoint}`, {
method: "POST",
body: JSON.stringify(data),
});
},
};
}
const api = createApiClient("https://api.example.com");
api.get("/users");
api.get("/products");
createApiClient() finishes running the moment it returns, but the object it hands back still remembers baseUrl. That's the closure: get and post keep access to a variable from a scope that's technically already finished executing.
The reason this matters beyond the definition is what it lets you do: create multiple independent clients from the same function, each remembering its own configuration.
const productionApi = createApiClient("https://api.example.com");
const stagingApi = createApiClient("https://staging.example.com");
Each one is a separate closure over a different baseUrl, with zero shared state between them. This is the same shape I'd reach for anywhere a function needs to remember configuration without a class: event handlers, callbacks, factories, or anything that needs private state without exposing it directly.
Promise.all, when requests don't depend on each other
Promise.all() takes an array of promises and resolves once every one of them has resolved, returning their results in the same order. If any single promise in the array rejects, Promise.all() rejects immediately, discarding the results of everything else.
Formgrid's dashboard loads several independent things on page load: lead pipeline stats, recent submissions, integration status, and account usage. None of them depend on each other's results.
The naive version waits for each one in sequence:
const stats = await fetchPipelineStats();
const submissions = await fetchRecentSubmissions();
const integrations = await fetchIntegrationStatus();
const usage = await fetchAccountUsage();
If each of those takes something like 500ms, 800ms, 600ms, and 300ms, waiting for them one at a time adds up to roughly 2.2 seconds before the dashboard can render anything.
const [stats, submissions, integrations, usage] = await Promise.all([
fetchPipelineStats(),
fetchRecentSubmissions(),
fetchIntegrationStatus(),
fetchAccountUsage(),
]);
Run concurrently instead; the total time is close to the slowest single request, around 800ms, not the sum of all four. Promise.all is the right tool specifically when the requests are genuinely independent, and you need all of them to proceed.
Promise.allSettled, when one failure shouldn't take down the rest
Promise.allSettled() also takes an array of promises, but it waits for every one of them to finish regardless of outcome, and returns an array describing each result as either fulfilled, with its value, or rejected, with its reason. Nothing gets discarded because one promise failed.
The catch with Promise.all is that a single rejection fails the entire batch. That's fine when every result is required before anything can render. It's the wrong choice when one piece failing shouldn't block the others.
Say the AI Smart Inbox category breakdown widget on that same dashboard has a bad moment and its request fails, while the lead stats, submissions, and integrations requests all succeed fine. With Promise.all, that one failure would throw away three perfectly good results along with it.
const results = await Promise.allSettled([
fetchPipelineStats(),
fetchRecentSubmissions(),
fetchIntegrationStatus(),
fetchAiInboxBreakdown(),
]);
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("Loaded:", result.value);
} else {
console.error("Failed:", result.reason);
}
});
Now the dashboard can render the three sections that succeeded and show a quiet error state only where the AI breakdown actually failed. Promise.allSettled is the right call whenever a partial result is genuinely more useful than an all-or-nothing failure.
The event loop isn't just a diagram; it's why setTimeout is deliberately used in real apps
The event loop is the mechanism that lets JavaScript, a language that runs on a single thread, handle asynchronous work without blocking. It continuously checks whether the call stack is empty, and once it is, pulls the next pending callback onto the stack to run, microtasks first, then the task queue where things like setTimeout callbacks wait.
Start with a concept that trips people up: fire and forget.
sendEmail();
console.log("Done");
If sendEmail() starts an asynchronous operation and you don't await it, your code doesn't wait around for it to finish. It starts the operation, moves on immediately, and the email finishes sometime later, off to the side. Compare that to:
await sendEmail();
console.log("Done");
which waits for the email to fully send before moving on. The fire-and-forget version is genuinely useful in production; it's the same pattern behind background jobs that shouldn't block a response, but it comes with a real catch: if that unawaited promise rejects, you get an unhandled rejection nobody's watching for. Fire and forget on purpose, with a .catch() attached, is a deliberate choice. Fire and forget by accident, because you forgot the await, is a bug waiting to surface in production logs.
That same asynchronous mental model is exactly what makes setTimeout more than an interview trivia question. It's genuinely useful, constantly, in real applications.
Showing a temporary success message. Say a form submission on Formgrid succeeds and you want to show "Form created successfully" for three seconds:
function showSuccessMessage() {
const message = document.querySelector("#success");
message.style.display = "block";
setTimeout(() => {
message.style.display = "none";
}, 3000);
}
JavaScript doesn't freeze for three seconds waiting for this. It shows the message, keeps doing whatever else it needs to do, and the event loop picks the hide callback back up once the timer's done and the call stack is free.
Debouncing a search box. This is the one that shows up in real products constantly. If a user types "formgrid" one character at a time, you don't want to fire an API request on every keystroke:
let timeout;
function searchUsers(query) {
clearTimeout(timeout);
timeout = setTimeout(() => {
fetch(`/api/users?search=${query}`);
}, 500);
}
Every keystroke cancels the previous timer and starts a new one. Only once the user actually stops typing for 500 milliseconds does the request go out.
Retrying a failed request after a delay.
async function fetchData() {
try {
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error("Request failed");
}
return await response.json();
} catch (error) {
console.log("Retrying...");
setTimeout(() => {
fetchData();
}, 2000);
}
}
setTimeout here is scheduling future work deliberately, giving a flaky dependency a couple of seconds to recover before trying again, rather than hammering it immediately.
Breaking up expensive work so the page doesn't freeze.
function processChunk() {
// process some work
if (moreWork) {
setTimeout(processChunk, 0);
}
}
processChunk();
The 0 here doesn't mean "run immediately." It means "let the current work finish and give the event loop a chance to handle anything else that's queued before running this next chunk."
Microtasks jump the queue ahead of setTimeout, even at zero delay
Here's the example that actually separates people who've memorized the event loop diagram from people who've internalized it. Imagine a user clicks "Generate Report":
console.log("Starting report");
setTimeout(() => {
console.log("Report notification");
}, 0);
Promise.resolve().then(() => {
console.log("Update UI");
});
console.log("Request submitted");
The output is:
Starting report
Request submitted
Update UI
Report notification
Not what most people guess on first read. The synchronous lines run first, in order: Starting report, then Request submitted. Then, before the setTimeout callback gets anywhere near the call stack, the promise's .then() callback runs, because promise callbacks go into the microtask queue, and the microtask queue is fully drained before the event loop even looks at the task queue where setTimeout callbacks live. Only after that does Report notification finally run.
This is exactly why a setTimeout(fn, 0) never means "run this immediately." It means "run this after the current synchronous code finishes, and after every pending microtask has already run." Debugging unexpected execution order in a React app, a Node service, or anything mixing promises with timers almost always comes back to this exact ordering.
If you're asked about this in an interview, debouncing is the strongest real-world example for setTimeout specifically, and this report generation example is the strongest one for explaining why microtasks and macrotasks aren't the same queue.
any versus unknown in TypeScript
any and unknown are both TypeScript types that can hold a value of any shape. The difference is what TypeScript does with that value afterward: any turns off type checking for it entirely, while unknown requires you to narrow it, prove what it actually is, before you're allowed to use it.
any tells TypeScript: don't check this value, I know what I'm doing.
let value: any = "hello";
value.foo.bar.baz();
TypeScript won't say a word about this. JavaScript will still crash at runtime because a string doesn't have .foo.bar.baz(), TypeScript just isn't watching anymore once something is typed any.
unknown takes the opposite position: I don't know what this is yet, so prove it before you use it.
const response = await fetch("/api/user");
const data: unknown = await response.json();
You don't actually know that the server returned what you expected, just because the request succeeded. Typing this any tells TypeScript to trust it completely. Typing it unknown forces you to check before you can touch it:
if (
typeof data === "object" &&
data !== null &&
"name" in data
) {
console.log(data.name);
}
Once narrowed like this, TypeScript trusts you again, but only inside that branch, and only as far as what you actually checked for.
This matters most at boundaries you don't control:
Your TypeScript application
↓
External API
↓
JSON
↓
unknown
↓
Validate or narrow
↓
Trusted data
API responses, user input, third-party libraries, anything read from a database or a file, none of it should be trusted at the type level just because it compiled. unknown forces the check. any lets you skip it and find out at runtime instead, usually in production, usually at a worse time than during development.
The short version worth remembering: any says trust me, TypeScript stops protecting you. unknown says I don't know yet, TypeScript makes you prove it.
Generics in TypeScript
Generics let you write a function, class, or interface that works with more than one type while still keeping full type information, using a placeholder type, conventionally written T, that gets filled in with a real type at the point where the code is actually used.
The idea behind generics is simpler than the syntax makes it look: write code once, and let it preserve whatever specific type it's handed, instead of losing that information.
Without generics:
function first(items: any[]) {
return items[0];
}
const result = first([1, 2, 3]);
result is typed any here. The function works, but you've thrown away the fact that you started with an array of numbers.
With a generic:
function first<T>(items: T[]): T {
return items[0];
}
const number = first([1, 2, 3]); // T = number
const name = first(["Allen", "John"]); // T = string
<T> is a placeholder: give me a type, and I'll use that exact type everywhere I've written T. TypeScript figures out what T is from what you actually pass in, and carries that information through to the return type.
The most common real-world shape of this is an API response wrapper:
interface ApiResponse<T> {
data: T;
success: boolean;
message?: string;
}
const response: ApiResponse<User> = {
success: true,
data: { id: 1, name: "Allen" },
};
ApiResponse<User>, ApiResponse<Product>, and ApiResponse<Order> all reuse the same interface, fully typed, instead of writing three nearly identical ones.
The same idea shows up just as often on the frontend, in a reusable table or list component:
<DataTable<User> data={users} />
<DataTable<Product> data={products} />
Same component, same rendering logic, but each usage stays fully typed to the data it's actually given. This is why generics show up everywhere in API clients, reusable components, and utility functions in general; they're the mechanism that keeps one piece of code reusable without becoming untyped in the process.
Where this connects
None of these ideas are actually separate from each other. const and let are about being explicit regarding what's allowed to change. Closures are about a function remembering the scope it was created in, even after that scope has technically finished running. Promise.all and Promise.allSettled are both about running independent work concurrently; they just disagree on what to do when one piece fails. The event loop underneath all of it decides the actual order any of this code runs in: synchronous first, then every microtask, then the task queue. unknown exists because you sometimes genuinely don't know a type yet and need to prove it before using it. Generics exist because you often do know the type at the call site and don't want to throw that information away by writing something that treats everything as any.
That's the pattern worth carrying into the next post in this series: data structures, algorithms, and testing as they actually show up in production, not as isolated interview trivia.
If there's a concept here you'd explain differently, or a production example you think makes any of this land better, I'd like to hear it. Reach me at allen@formgrid.dev.