Skip to content
Interview prep ยท questions

JavaScript interview questions

24 questions, from core to senior. Say your answer out loud first, then open the model answer and check it against the points a strong answer covers.

What is a closure, and what does this code log?

  • Core
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
Show the model answer

A closure is a function that keeps access to the variables of the scope where it was created, and this code logs 3, 3, 3. The function holds a reference to that scope, not a copy of the values. So it can still read and change those variables after the outer function has returned. Here var creates one variable i for the whole loop, and all three callbacks close over it. They run after the loop has finished, when i is already 3. With let, every iteration gets its own i, so the code logs 0, 1, 2. A practical use is debounce: the timer id lives in the closure, so it stays private and survives between calls. React components depend on closures too. A callback created in an old render still sees the old state, which is called a stale closure.

A strong answer covers

  • Defines a closure: a function plus the scope it was created in
  • Says the function keeps a reference to the variables, not a copy
  • Gives 3, 3, 3 and explains that var creates one shared variable
  • Fixes it with let: a new binding for every iteration
  • Names a real use, such as debounce or private state

Likely follow-up

What is a stale closure in a React effect, and how do you fix it?

What is the difference between var, let and const, and what is hoisting?

  • Core
Show the model answer

var is function-scoped, let and const are block-scoped, and hoisting means that declarations are registered before the code in their scope runs. A var declared inside an if or a for block is visible in the whole function. let and const exist only inside the nearest pair of braces. A var is hoisted and starts as undefined, so reading it early gives undefined, not an error. let and const are hoisted too, but they stay uninitialized until the line of the declaration. That period is the temporal dead zone, and reading the variable there throws a ReferenceError. Function declarations are hoisted together with their body, so they can be called before they appear. const blocks reassignment, but it does not freeze the value: the properties of a const object can still change. The practical rule is const by default, let when a value must be reassigned, and no var.

A strong answer covers

  • Gives function scope for var and block scope for let and const
  • Says var is hoisted and starts as undefined
  • Explains the temporal dead zone and its ReferenceError
  • Notes that function declarations are hoisted with their body
  • Says const fixes the binding, not the contents of an object

Likely follow-up

What happens when you use typeof on a variable in its temporal dead zone, and on one that was never declared?

How does JavaScript decide the value of this, and what does this code log?

  • Core
"use strict";
const user = {
  name: "Ana",
  greet() {
    return this.name;
  },
};
const greet = user.greet;
console.log(user.greet());
console.log(greet());
Show the model answer

this is decided by how a function is called, not by where it is defined, so the code logs Ana and then throws a TypeError. A method call, like user.greet(), sets this to the object before the dot. A plain call, like greet(), sets this to undefined in strict mode and to the global object in sloppy mode. A call with new sets this to the new object. call, apply and bind set it explicitly. Arrow functions are the exception: they have no this of their own and use the this of the scope where they are written. In the snippet, the second call is a plain call in strict mode, so this is undefined and reading name fails. Modules and class bodies are always strict. This is why a class method passed as a callback loses its this, unless it is bound or wrapped in an arrow function.

A strong answer covers

  • Says this depends on how the function is called, not where it is defined
  • Covers the method call, the plain call, new, and call, apply and bind
  • Explains that an arrow function takes this from the enclosing scope
  • Gives the output: Ana, then a TypeError because this is undefined
  • Knows the plain call gives the global object in sloppy mode

Likely follow-up

What are two ways to make the second call log Ana as well?

What do call, apply and bind do, and when do you still need bind?

  • Core
Show the model answer

All three set the value of this for a function: call and apply run the function immediately, and bind returns a new function for later. call takes the arguments one by one, and apply takes them as one array. bind fixes this, so the new function can be passed around safely. A real use is a class method that is passed as an event handler: bind makes it keep its object. bind can also fix the first arguments, which is partial application: multiply.bind(null, 2) returns a function that needs only the second number. Today spread syntax has replaced most uses of apply, and arrow functions have replaced many uses of bind. One detail: a bound function keeps its this. A second bind, or call with another object, does not change it.

A strong answer covers

  • Says call and apply run the function now, and bind returns a new function
  • Knows call takes separate arguments and apply takes an array
  • Gives a real use for bind: a method passed as a callback keeps its object
  • Shows partial application with bind
  • Notes that spread and arrow functions cover most cases today

Likely follow-up

Can you write a simple version of bind yourself?

How do arrow functions differ from regular functions, and when should you not use one?

  • Core
Show the model answer

The main difference is that an arrow function has no this of its own. It uses the this of the scope where it is written, and call, apply and bind cannot change it. It also has no arguments object, so you use rest parameters. It cannot be called with new, and it has no prototype property. This makes arrows a good fit for callbacks: inside a class method, an arrow passed to map or setTimeout still sees the this of the method. They are a poor fit for object methods, because this will not be the object, and for DOM listeners that expect this to be the element. Use event.currentTarget there. In a class, an arrow function in a field gives a handler that is always bound. The cost is that every instance gets its own copy of the function, instead of one shared method on the prototype.

A strong answer covers

  • Says an arrow has no own this and takes it from the surrounding scope
  • Mentions no arguments object, no new and no prototype property
  • Explains why arrows suit callbacks inside methods
  • Explains why arrows are wrong for object methods
  • Names the cost of arrow class fields: one function per instance

Likely follow-up

What is this inside an arrow function written at the top level of an ES module?

How does prototypal inheritance work, and what does the class syntax add?

  • Core
Show the model answer

Objects inherit through the prototype chain, and class is mostly a cleaner syntax for the same mechanism. An object has an internal link to another object, its prototype. When you read a property, JavaScript looks at the object itself, then at its prototype, then at the next one, until it finds the property or reaches null. Methods are shared this way: they exist once on the prototype, not on every instance. With class, methods go on the prototype, extends links two prototypes, and super calls the parent. Classes also add real features: private fields that start with #, static members, and the rule that a class cannot be called without new. In UI code, deep inheritance is hard to change, because a child depends on the details of every parent. Composition, which means combining small functions or objects, is usually easier. React hooks are an example of it.

A strong answer covers

  • Describes the prototype chain and that the lookup ends at null
  • Says methods live once on the prototype, not on each instance
  • Explains class as syntax over prototypes, with extends and super
  • Names what class really adds: private fields, static members, new is required
  • Compares inheritance with composition and gives a reason to prefer composition

Likely follow-up

What is the difference between an object's prototype and the prototype property of a function?

In what order does this code log, and why?

  • Core
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve()
  .then(() => console.log("3"))
  .then(() => console.log("4"));
console.log("5");
Show the model answer

It logs 1, 5, 3, 4, 2. JavaScript runs your code on one thread with one call stack. First the synchronous code runs to the end, so 1 and 5 are logged. The setTimeout callback goes to the task queue, and the promise callback goes to the microtask queue. When the call stack is empty, the event loop runs all microtasks before it takes the next task. So 3 runs, and it queues 4 as a new microtask, which also runs before any task. Only then does the timer callback log 2, even with a delay of zero. Promise callbacks and the code after an await are microtasks. Timers and DOM events are tasks. The browser can render only between tasks, after the microtasks are done. So one long synchronous function, or microtasks that keep adding new microtasks, will block rendering and user input.

A strong answer covers

  • Gives the order: 1, 5, 3, 4, 2
  • Names the call stack, the task queue and the microtask queue
  • Says all microtasks run before the next task, including new ones
  • Puts promise callbacks in microtasks and timers and events in tasks
  • Links it to rendering: long synchronous work blocks the page

Likely follow-up

How would you split a long calculation so that the page stays responsive?

What is a promise, and how does an error travel through a promise chain?

  • Core
Show the model answer

A promise is an object that represents a value that will be ready later, and an error travels down the chain until the first catch. A promise has three states: pending, fulfilled and rejected. Once it is settled, its state and value never change. Every call to then returns a new promise, which is what makes chaining work. If the callback returns a value, the next step receives it. If it returns a promise, the chain waits for it. If it throws, the new promise is rejected. A rejection skips every then that has only a success callback. catch also returns a promise, so the chain can continue after it. The second argument of then handles a rejection from earlier steps, but not an error thrown by the first callback of the same then. A catch at the end covers both, so one final catch is the usual pattern.

A strong answer covers

  • Names the three states and says a settled promise never changes
  • Says then returns a new promise, which makes chaining possible
  • Covers the three results of a callback: a value, a promise, a thrown error
  • Describes how a rejection skips to the nearest catch
  • Explains why then(ok, fail) misses an error thrown inside ok

Likely follow-up

What happens if you forget to return the inner promise inside a then callback?

What does async/await add to promises, and how do these two functions differ?

  • Core
async function one() {
  const user = await getUser();
  const posts = await getPosts();
  return [user, posts];
}

async function two() {
  const requests = [getUser(), getPosts()];
  const [user, posts] = await Promise.all(requests);
  return [user, posts];
}
Show the model answer

async/await is syntax on top of promises: an async function always returns a promise, and await pauses the function until a promise settles. The thread is not blocked, because the rest of the function continues later as a microtask. In the snippet, function one is sequential. The second request starts only after the first has finished, so the total time is the sum of both. Function two starts both requests first and then waits for them together, so the total time is that of the slower one. Sequential awaits are right only when one call needs the result of the other. For errors, a rejected promise makes await throw, so a normal try/catch works. Two things to remember: Promise.all rejects as soon as one promise rejects, and a promise that is not awaited is not covered by the try/catch around it.

A strong answer covers

  • Says an async function always returns a promise
  • Explains that await pauses the function, not the thread
  • Identifies function one as sequential and function two as parallel
  • Compares the total time: the sum against the slower request
  • Handles errors with try/catch around the await

Likely follow-up

What goes wrong when you use await inside forEach, and what do you use instead?

What is the difference between == and ===, and where does Object.is differ from both?

  • Core
Show the model answer

=== compares two values without converting types, and == first converts them to a common type when the types differ. Object.is is like === except for NaN and the sign of zero. So 1 == '1' and 0 == '' are true, and with === both are false. The conversion rules are hard to remember, so the usual rule is === everywhere. One accepted exception is x == null, which is true only for null and undefined. Coercion also happens in conditions. The falsy values are false, 0, -0, 0n, the empty string, null, undefined and NaN. Everything else is truthy, including an empty array and an empty object. NaN is not equal to itself, so test for it with Number.isNaN. With Object.is, NaN equals NaN, and 0 and -0 are different. React uses Object.is to compare state values and hook dependencies.

A strong answer covers

  • Says === does not convert types and == does
  • Lists the falsy values and knows that [] and {} are truthy
  • Says NaN is not equal to itself and names Number.isNaN
  • Gives the two differences of Object.is: NaN and the sign of zero
  • States a rule: === by default, x == null as the only exception

Likely follow-up

Why can count && <List /> show a 0 on the page in React, and how do you fix it?

What does this code log, and how would you make a full copy of the object?

  • Core
const a = { name: "Ana", tags: ["x"] };
const b = { ...a };
b.name = "Bo";
b.tags.push("y");
console.log(a.name, a.tags);
Show the model answer

It logs Ana and an array with x and y, because spread copies only one level. For a full copy, use structuredClone. Primitives, such as numbers and strings, are copied by value. Objects, arrays and functions are held by reference: assigning one to a new variable copies only the reference, so both names point to the same object. A spread copy is shallow. The new object is separate at the top level, but nested objects are still shared, so b.name does not change a, and the push to b.tags does. structuredClone handles nested objects, dates, maps, sets and circular references. It throws on functions and DOM nodes, and it does not keep the prototype of a class instance. The older trick, JSON.parse(JSON.stringify(x)), drops undefined values and functions, turns dates into strings, and throws on circular references. In React state, copying only the path that changes is usually enough.

A strong answer covers

  • Gives the output and explains why name is safe but tags is shared
  • Separates primitives (copied by value) from objects (shared by reference)
  • Says spread makes a shallow copy: one level only
  • Names structuredClone and a limit: functions, DOM nodes or class prototypes
  • Lists what the JSON trick loses: undefined, functions, dates

Likely follow-up

How do you update one field of a nested object in React state without changing the original?

Which array methods change the original array, and which return a new one?

  • Core
Show the model answer

sort, reverse and splice change the array in place, and so do push, pop, shift and unshift. map, filter, reduce, find, some and every do not change it. map returns a new array of the same length with every item transformed. filter returns a new array with the items that pass a test. reduce combines all items into one value, such as a total or a lookup object. find returns the first matching item or undefined. some and every return a boolean and stop as soon as the answer is known. The difference matters in React: sorting an array from props or state in place mutates data that React treats as read-only. The newer methods toSorted, toReversed and toSpliced return a changed copy instead. One more trap: sort without a compare function compares items as strings, so 10 comes before 2.

A strong answer covers

  • Lists the mutating methods: sort, reverse, splice, push, pop, shift, unshift
  • Explains map, filter and reduce in one sentence each
  • Knows find returns an item or undefined, and some and every return a boolean
  • Names toSorted, toReversed and toSpliced as the copying versions
  • Mentions that the default sort compares items as strings

Likely follow-up

When is a plain for...of loop a better choice than reduce?

How do destructuring, rest and spread work, and how deep does a spread copy go?

  • Core
Show the model answer

Destructuring unpacks values from an object or an array into variables, rest collects the remaining items, and spread expands items into a new array or object, one level deep. When you destructure, you can rename a property and give it a default value. The default is used only when the value is undefined, not when it is null. Rest and spread use the same three dots but do opposite jobs. Rest appears in a parameter list or a destructuring pattern and gathers what is left into a new array or object. Spread puts array items or object properties into a new array, object or function call. In React, const { id, ...rest } = props is rest, and { ...state, name } is spread. With object spread, later properties win, so the order matters. Nested objects are not copied: they are still shared with the original.

A strong answer covers

  • Shows destructuring with a rename and a default value
  • Says a default applies to undefined but not to null
  • Separates rest, which collects, from spread, which expands
  • Notes that later properties win in object spread
  • States that spread makes a shallow copy

Likely follow-up

What happens when you destructure a property from null or undefined, and how do you guard against it?

How do ES modules differ from CommonJS, and why does that matter for bundle size?

  • Core
Show the model answer

ES modules are static and CommonJS is dynamic, and that is what makes tree shaking possible. ES modules use import and export statements that are known before the code runs. CommonJS uses require and module.exports, which are a normal function call and a normal object at run time. So a require can sit inside an if, and its path can be computed. Because ES modules are static, a bundler can see which exports are never imported and remove them. Code with side effects on import must be kept, so tree shaking works best with modules that have none. ES modules are also always in strict mode, export live bindings instead of copied values, and allow top-level await. Dynamic import() is an expression that returns a promise for the module. Bundlers put that module in a separate chunk, which is how route-based code splitting and React.lazy work.

A strong answer covers

  • Contrasts static import and export with run-time require
  • Explains tree shaking as a result of the static structure
  • Names side effects as the thing that blocks tree shaking
  • Says import() returns a promise and creates a separate chunk
  • Adds one more ESM fact: strict mode, live bindings or top-level await

Likely follow-up

A library you import is not tree-shaken. What would you check?

How do you handle errors in asynchronous JavaScript code?

  • Core
Show the model answer

I use try/catch with await, a final catch on promise chains, and a global handler for anything that was missed. try/catch catches an asynchronous error only when the promise is awaited inside the try block. If an async function is called without await, or an error is thrown inside a setTimeout callback, the try block has already finished and the catch never runs. For errors that mean something to the app, I create classes that extend Error, such as an HttpError with a status code. The caller checks them with instanceof. When I catch a low-level error and throw a higher-level one, I pass the original as cause in the options of the Error constructor. Then the first stack trace is not lost. A rejected promise with no handler fires the unhandledrejection event on window in a browser. In Node it ends the process by default.

A strong answer covers

  • Says try/catch needs an await inside the try to catch async errors
  • Gives a case that escapes try/catch: a missing await or a timer callback
  • Uses custom Error classes and instanceof to tell errors apart
  • Keeps the original error with the cause option
  • Names the unhandledrejection event and what Node does by default

Likely follow-up

Which errors does a React error boundary catch, and which ones does it miss?

What is the difference between debounce and throttle, and when do you use each?

  • Core
Show the model answer

Debounce runs a function once after the calls have stopped, and throttle runs it at most once per interval while the calls continue. With debounce, every new call resets a timer, so the function runs only after a quiet period. With throttle, the function runs at a steady rate, however many calls arrive. Use debounce when only the final value matters, such as a search input that sends a request after the user stops typing. Use throttle when you need regular updates during an activity, such as a scroll or resize handler. Both can run on the leading edge (the first call), the trailing edge (the end of the wait), or both. A search input normally uses the trailing call. In React, create the debounced function once, with useMemo or a ref, because a new one on every render has its own timer. Cancel it on unmount.

A strong answer covers

  • Says debounce runs once after the calls stop
  • Says throttle runs at most once per interval while calls continue
  • Matches debounce to search input and throttle to scroll or resize
  • Explains the leading and the trailing call
  • Keeps one debounced function across renders and cancels it on unmount

Likely follow-up

How would you implement debounce with a cancel method?

How does a DOM event travel through the page, and what is event delegation?

  • Core
Show the model answer

A DOM event travels in three phases, capturing, target and bubbling, and event delegation uses bubbling to handle many children with one listener on their parent. In the capturing phase the event goes down from the window to the target. After the target phase it bubbles back up through the parents. Listeners run in the bubbling phase by default, and in the capturing phase if you pass capture: true. With delegation, the listener on the parent reads event.target, often with closest, to find the clicked item. This needs fewer listeners and also works for items added later. stopPropagation stops the event from going further. preventDefault does not stop it. It cancels the default action of the browser, such as following a link or submitting a form. A few events, like focus and blur, do not bubble. React uses delegation itself: it attaches its listeners to the root container.

A strong answer covers

  • Names the three phases: capturing, target, bubbling
  • Explains delegation: one listener on a parent that checks event.target
  • Gives the benefits: fewer listeners, and it works for items added later
  • Separates stopPropagation (travel) from preventDefault (default action)
  • Knows that some events, like focus and blur, do not bubble

Likely follow-up

What is the difference between event.target and event.currentTarget?

What causes memory leaks in a single-page app, and how do you find one?

  • Senior
Show the model answer

A memory leak happens when objects that are no longer needed are still reachable, so the garbage collector cannot free them. The collector starts from roots, such as global variables and the call stack, and keeps everything it can reach from them. A single-page app is never reloaded, so small leaks keep growing. The common causes are listeners that are never removed, timers that are never cleared, open subscriptions, and caches or closures that hold large data. Another is detached DOM nodes: elements removed from the page but still referenced from JavaScript. In React the fix is usually the cleanup function of an effect. To find a leak, I repeat one action several times, such as opening and closing a dialog, and compare heap snapshots in Chrome DevTools. Objects whose count grows every time point to the leak, and the retainers list shows what still holds them.

A strong answer covers

  • Defines a leak through reachability: not needed but still referenced
  • Lists causes: listeners, timers, subscriptions, caches or closures, detached DOM nodes
  • Says why single-page apps suffer: the page is never reloaded
  • Fixes leaks with cleanup, such as the function returned from useEffect
  • Describes the method: repeat an action, compare heap snapshots, read the retainers

Likely follow-up

How can a WeakMap help when you cache data for objects or DOM nodes?

What are iterators and generators, and where is async iteration useful?

  • Senior
Show the model answer

An iterator is an object with a next method that returns a value and a done flag. A generator is a function that creates such an iterator for you. An object is iterable when it has a Symbol.iterator method that returns an iterator. for...of, spread and array destructuring all use this protocol, so they work on arrays, strings, maps and sets, but not on plain objects. A generator is written with function*. It pauses at every yield and continues when next is called, so values are produced lazily, one at a time. A sequence can even be infinite. Async iteration is the same idea with promises: an async generator can await between yields, and for await...of reads the values. A real use is a paginated API. The generator fetches the next page only when the consumer asks for more items, so the calling code is one simple loop.

A strong answer covers

  • Describes next() and its result with value and done
  • Says an iterable has a Symbol.iterator method and for...of uses it
  • Explains that a generator pauses at yield and produces values lazily
  • Covers async generators and for await...of
  • Gives a real use, such as reading a paginated API page by page

Likely follow-up

How would you make your own class work with for...of?

When would you choose a Map, a Set or a WeakMap over a plain object or an array?

  • Senior
Show the model answer

I choose a Map for dynamic or non-string keys, a Set for unique values, and a WeakMap to attach data to objects without keeping them in memory. A Map accepts any value as a key, including objects. It iterates in insertion order, has a size, and is built for frequent adds and deletes. A plain object allows only strings and symbols as keys. It also inherits keys such as toString from its prototype, which causes bugs with user-provided keys. It is still the right choice for a fixed shape and for JSON data. A Set gives a fast has check and easy removal of duplicates. A WeakMap holds its keys, which are objects, weakly: when nothing else references a key, its entry can be garbage collected. That suits a cache or private data for a DOM node. A WeakMap cannot be iterated and has no size.

A strong answer covers

  • Gives reasons for Map: any key type, insertion order, size, no inherited keys
  • Says when a plain object is still better: a fixed shape or JSON data
  • Uses Set for unique values and a fast has check
  • Explains weak keys: the entry can be collected when the key object is gone
  • Names a use for WeakMap and its limits: no iteration, no size

Likely follow-up

Why is it not possible to iterate over a WeakMap?

Why do React and state libraries need immutable updates, and what do they cost?

  • Senior
Show the model answer

Immutable updates make change detection cheap. If data is never changed in place, a new reference means a change and the same reference means no change, so one comparison replaces a deep one. React compares state and hook dependencies with Object.is, which for objects is a reference check. If you mutate an object and set it again, the reference is the same. React skips the render, and memo, useMemo and effects do not see the change either. To update immutably, you create a new object for every level on the path to the change and reuse everything else. That reuse is called structural sharing, and it keeps the copy small. The cost is more code for deeply nested data and some extra allocation. Flat state reduces it, and a library such as Immer lets you write code that looks like mutation but produces a new object.

A strong answer covers

  • Links immutability to cheap change detection by reference
  • Explains the bug after a mutation: no re-render and stale memoized values
  • Shows the method: copy every level on the path, reuse the rest
  • Uses the term structural sharing correctly
  • Names the cost and a way to reduce it: flat state or Immer

Likely follow-up

How does Immer produce a new object from code that looks like a mutation?

What do function composition and currying give you, and when would you avoid them?

  • Senior
Show the model answer

They let you build logic from small reusable functions, and I avoid them when a plain function with named variables is easier to read. Both use higher-order functions: functions that take a function as an argument or return one, like map or debounce. Composition builds a function from smaller ones: the output of one is the input of the next. compose applies them from right to left, and pipe from left to right. Currying turns a function with several arguments into a chain of functions that take one argument each, so add(1, 2) becomes add(1)(2). Partial application is more general: you fix some arguments now and pass the rest later, for example with bind. These tools help with small pure steps, such as validators or data transformations. They hurt when a long chain hides the data and makes stack traces and TypeScript errors hard to read.

A strong answer covers

  • Defines a higher-order function and gives an example
  • Knows the direction: compose is right to left, pipe is left to right
  • Separates currying (one argument at a time) from partial application
  • Gives a case where it helps: small pure steps that are reused
  • Gives a case where it hurts: long chains that are hard to debug or type

Likely follow-up

Can you write pipe with reduce?

What is the difference between Promise.all, allSettled, race and any?

  • Senior
Show the model answer

All four take a list of promises and return one promise, and they differ in when that promise settles. Promise.all fulfills with an array of all results, in input order, and rejects as soon as one promise rejects. Use it when you need every result, such as several requests for one page. Promise.allSettled never rejects. It waits for all and returns a status with a value or a reason for each one. Use it when partial success is acceptable, such as independent widgets on a dashboard. Promise.race settles like the first promise that settles, fulfilled or rejected. It is the classic way to add a timeout. Promise.any fulfills with the first promise that fulfills and ignores rejections. If all reject, it rejects with an AggregateError. Use it to try several sources. None of them cancels the other promises: the remaining requests keep running unless you abort them.

A strong answer covers

  • all: every result in input order, rejects on the first rejection
  • allSettled: never rejects, gives a status with a value or a reason
  • race: follows the first promise to settle, even if it rejects
  • any: the first fulfilled promise, AggregateError when all reject
  • Notes that no combinator cancels the remaining work

Likely follow-up

How would you run 100 requests with at most 5 of them active at the same time?

How do you cancel a fetch, and why does this effect never show a stale search result?

  • Senior
useEffect(() => {
  const controller = new AbortController();
  fetch("/api/search?q=" + encodeURIComponent(query), {
    signal: controller.signal,
  })
    .then((res) => res.json())
    .then(setResults)
    .catch((err) => {
      if (err.name !== "AbortError") setError(err);
    });
  return () => controller.abort();
}, [query]);
Show the model answer

You cancel a fetch with an AbortController: you pass its signal to fetch and call abort when the result is no longer needed. The promise then rejects with an error named AbortError, which the code should ignore and not show to the user. In a search box, responses can arrive in a different order than the requests were sent, so a slow old response could replace the result of the newest query. In the snippet, the cleanup of the effect aborts the previous request every time the query changes, and also on unmount. So only the latest request can set the state. For a time limit, AbortSignal.timeout returns a signal that aborts by itself after the given number of milliseconds, with a TimeoutError. AbortSignal.any combines several signals. One limit: abort only stops the client from waiting, and the server may still finish its work.

A strong answer covers

  • Creates an AbortController and passes its signal to fetch
  • Ignores the AbortError instead of showing it as a failure
  • Explains the race: responses can arrive out of order
  • Aborts the previous request in the effect cleanup when the query changes
  • Names AbortSignal.timeout for a time limit

Likely follow-up

If a request could not be aborted, how else would you ignore a stale response?