JavaScript interviews for 3 years of experience care less about definitions and more about what happened in your code: a date that showed the wrong day, a fetch call that failed quietly, a test that only broke in the full suite, a page that froze. It is written for JavaScript developers with about two to four years of real work, the stage where you own features end to end but someone else still designs the system. Each question shows what the interviewer is checking, the shape of a good answer and a short spoken answer. Swap the stories for your own before the interview.
Search all questions by round, difficulty and level, or save the ones you want to practice.
Cause: every number is a 64-bit float, so values like 0.1 and 0.2 are stored slightly off and the error shows up after adding.
Fix: keep amounts as whole numbers in the smallest unit and only divide when you display.
Display: format with Intl.NumberFormat, and be careful with toFixed, which returns a string and rounds the stored binary value.
“Every JavaScript number is a 64-bit float, so 0.1 and 0.2 can't be stored exactly, and adding them gives 0.30000000000000004. On one project we summed item prices as decimals and rounded at the end with toFixed, and some totals were off by a cent. toFixed rounds the binary value, which is why 1.005 toFixed 2 gives 1.00, not 1.01. The fix was to keep every amount as a whole number in the smallest unit, so integer maths stays exact, and only divide by 100 when formatting. We used Intl.NumberFormat for the display so the symbol and separators matched the user's locale. We also made the API send amounts as integers, so the front end never had to guess.”
// amounts stored as whole minor units, never as decimals
const totalMinor = cart.reduce((sum, item) => sum + item.priceMinor * item.qty, 0);
const formatter = new Intl.NumberFormat(userLocale, { style: 'currency', currency: currencyCode });
const shown = formatter.format(totalMinor / 100);
Calling it a JavaScript bug, or fixing it only by adding toFixed(2) at the end of every calculation.
Cause: a date-only ISO string is parsed as midnight UTC, while a date-time string with no offset is parsed as local time.
Symptom: displayed in a time zone behind UTC, midnight UTC is still the previous evening, so the day slips back.
Fix: keep calendar dates as plain strings or build them in local time, and send real moments as ISO strings with an offset.
“The string '2026-03-05' is a date-only ISO string, and JavaScript parses that as midnight UTC. When we formatted it for users in a time zone behind UTC, midnight UTC was still the evening of March 4 for them, so the date showed a day early. The confusing part is that '2026-03-05T00:00' without an offset is parsed as local time, so two strings that look almost the same behave differently. A due date is a calendar date, not a moment in time, so I stopped turning it into a Date at all. We kept it as the plain string for storage and display, and when we needed date maths we built it with new Date(year, month - 1, day), which is local. Real timestamps, like when something was created, we send as full ISO strings with a Z.”
Fixing it by adding or subtracting hours by hand, which breaks again for another time zone or at a daylight saving change.
Strings only: localStorage stores strings, so objects go through JSON.stringify and JSON.parse.
What JSON loses: Dates become ISO strings, Maps and Sets become empty objects, undefined and functions are dropped, and BigInt throws.
Fix: convert those fields on the way in and out, keep a version number, and wrap writes in try/catch.
“localStorage only stores strings, so we saved settings with JSON.stringify and read them back with JSON.parse. JSON has no Date type, so the Date was written as an ISO string and came back as a plain string, and the code calling getTime on it crashed. The Map was worse: JSON.stringify turns a Map into an empty object, so the user's saved filters just disappeared. Undefined fields and functions get dropped too, and a BigInt makes stringify throw. My fix was a small pair of functions that turn the settings into a plain shape, with the date as a string and the Map as an array of pairs, and rebuild the real types when loading. I also stored a version number so old saved data could be migrated, and wrapped every storage call in try/catch, because it throws when storage is full or blocked.”
Expecting localStorage to store objects as they are, or trusting whatever comes back from JSON.parse without checking its shape.
Default sort: with no compare function, sort turns values into strings and compares UTF-16 code units, so '100' comes before '20'.
Numbers: pass a compare function that subtracts.
Text: use localeCompare or an Intl.Collator so accents and case sort the way people expect.
“When you call sort with no compare function, JavaScript converts every element to a string and compares code units. So 100 becomes '100', and '1' is less than '2', which puts 100 before 20. For prices I pass a compare function that returns a minus b. The names problem is the same default: an accented capital like É has a higher code unit than every plain letter, so it lands after Z, and capitals sort before lowercase. For text I use an Intl.Collator, which knows the rules for the user's language, and I create it once rather than calling localeCompare inside every comparison on a big list. And I use toSorted, so the list I was given stays in its original order.”
[100, 20, 3].sort(); // [100, 20, 3], compared as strings
const byPrice = products.toSorted((a, b) => a.price - b.price);
const collator = new Intl.Collator(userLocale, { sensitivity: 'base' });
const byName = products.toSorted((a, b) => collator.compare(a.name, b.name));
Not knowing why the numbers came out in that order, or writing a compare function that returns true and false.
Cause: an ampersand starts a new parameter, a hash starts the fragment that is never sent to the server, and a plus sign is often read as a space.
Fix: build the URL with the URL class and set values through searchParams, which escapes each one.
Single values: encodeURIComponent works too, but encodeURI is the wrong tool because it leaves ampersands and hashes alone.
“The old code did '/api/search?q=' plus whatever the user typed. For 'R&D', the ampersand ends the q parameter, so the server saw q as just R and a stray parameter called D. For 'C#', the hash starts the fragment, and the browser never sends the fragment to the server, so it searched for C. A plus sign had its own problem, because many servers read it as a space. After that I stopped gluing user text into URLs by hand. I build them with the URL class and set each value through searchParams, which escapes it properly, and fetch accepts the URL object directly. If I only need to escape one value, encodeURIComponent is fine, but encodeURI is the trap, because it leaves ampersands and hashes as they are.”
const url = new URL('/api/search', window.location.origin);
url.searchParams.set('q', query); // 'R&D' and 'C#' arrive intact
url.searchParams.set('page', String(page));
const res = await fetch(url);
Building URLs by joining strings and patching only the one character that broke.
Cause: a plain object inherits properties, so counts['constructor'] already holds the Object function before you set anything.
Result: the function plus one becomes string concatenation, and a key like '__proto__' is ignored or worse.
Fix: use a Map for data keyed by user input, or Object.create(null), and check own keys with Object.hasOwn.
“The code did counts[word] = (counts[word] || 0) + 1. For most words the lookup is undefined, so it starts at zero. But a plain object inherits from Object.prototype, and that has a constructor property pointing at the Object function. So counts['constructor'] was already truthy, and the function plus one turned into a long string ending in 1. The word '__proto__' was worse: assigning a string to it is silently ignored, so that count just vanished. Any time the keys come from users or outside data, I use a Map now. It has no inherited keys, any value can be a key, and it has a proper size and get and set. If I need a plain object, say for JSON, I create it with Object.create(null).”
const counts = new Map();
for (const word of words) {
counts.set(word, (counts.get(word) ?? 0) + 1);
}
counts.get('constructor'); // a real number now
Saying 'constructor' is a reserved word, or not knowing that objects inherit properties at all.
HTTP errors: fetch resolves on a 404 or 500, so check response.ok and throw yourself.
Timeouts: there is no default timeout, so pass an abort signal.
One place: a small wrapper that every call uses, with the status attached to the error for the caller.
“The surprise with fetch is that a 404 or a 500 is not a rejection. The promise resolves as long as a response came back, so our code happily called json on an error body and rendered an empty table. It only rejects when the request can't complete, like a network failure or an abort. So I wrote one wrapper: it checks response.ok and throws an error that carries the status, so callers can treat a 404 differently from a 500. For the hanging request, fetch has no timeout, so I pass AbortSignal.timeout, which aborts the request and rejects with a TimeoutError. Every screen then used this wrapper, and the table showed a proper error message with a retry button instead of looking empty.”
async function getJson(url, { timeoutMs = 8000 } = {}) {
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
if (!res.ok) {
const err = new Error('Request failed with status ' + res.status);
err.status = res.status;
throw err;
}
return res.json();
}
Believing a .catch on fetch will catch a 500, or having no idea how to stop a request that never answers.
Cause: forEach calls the async callback and throws away the promise, so nothing waits.
Errors: a rejection inside becomes an unhandled rejection, and a try/catch around forEach never sees it.
Fix: for...of with await to go one at a time, or await Promise.all over map to run them together.
“forEach doesn't know anything about promises. It calls the async callback for each item, each call returns a promise, and forEach just discards them. So the loop finishes straight away and 'All saved' shows while every save is still in flight. The failures were lost for the same reason: a rejected promise nobody is holding becomes an unhandled rejection, and the try/catch around the forEach had already finished. The fix depends on what you need. If order matters, or the server can't take many requests at once, I use a for...of loop with await, which runs them one after another. If they're independent, I map the items to promises and await Promise.all, and then the catch really does see a failure. We also added a lint rule that flags async callbacks passed to forEach.”
// one after another
for (const item of items) {
await save(item);
}
// all together, and a failure reaches the catch
await Promise.all(items.map(item => save(item)));
showMessage('All saved');
Adding await in front of forEach and expecting it to wait.
Workers: start a fixed number of runners that each pull the next item from a shared index.
Order: write each result at its own index, so the output matches the input.
Failure: decide whether one failure stops everything or is recorded per item.
“I start a fixed number of runners, five here, and they share one index. Each runner takes the next item, awaits the upload and loops until nothing is left. Taking the index and moving it on happens in one synchronous step, and JavaScript runs one piece of code at a time, so two runners can never grab the same file. I write each result at the item's own position, so the results come back in input order even though uploads finish in any order. Then I await Promise.all on the runners. As written, one failed upload rejects the whole thing, though the other runners keep going. For uploads I actually catch inside the worker and record success or failure per file, so the user sees which ones to retry.”
async function mapWithLimit(items, limit, worker) {
const results = new Array(items.length);
let next = 0;
async function runner() {
while (next < items.length) {
const i = next++;
results[i] = await worker(items[i], i);
}
}
const count = Math.min(limit, items.length);
await Promise.all(Array.from({ length: count }, () => runner()));
return results;
}
Splitting the files into fixed batches of five and waiting for each whole batch, without noticing one slow file holds up the other four.
What to retry: timeouts, network errors, 5xx and 429, never a 400 or a 401.
How: a capped number of attempts with a growing wait and a bit of randomness.
Safety: only repeat calls that are safe to repeat, such as reads or writes with an idempotency key.
“I wrap the call in a loop with a maximum number of attempts. If it fails, I first ask whether retrying could help. A timeout, a network error, a 5xx or a 429 might pass next time. A 400 or a 401 will fail the same way forever, so I throw straight away. Between attempts I wait longer each time and add some randomness, so a hundred browsers that failed together don't all hit the server again at the same moment. The part I'd stress in an interview is which calls to retry. Reading data is safe. Creating an order or charging a card isn't, unless the API accepts an idempotency key so a repeated request is recognised. Without that, a retry after a timeout can create the same order twice.”
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
function isRetriable(err) {
if (err.name === 'TimeoutError') return true;
if (err.status === undefined) return err.name === 'TypeError'; // network failure
return err.status === 429 || err.status >= 500;
}
async function withRetry(fn, attempts = 3, baseMs = 300) {
for (let attempt = 1; ; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt >= attempts || !isRetriable(err)) throw err;
await sleep(baseMs * 2 ** (attempt - 1) + Math.random() * baseMs);
}
}
}
Retrying every error in a tight loop with no wait and no limit, including requests that create or charge something.
Timers: switch to fake timers and move the clock forward by hand.
Network: mock fetch, or the small module that wraps it, and return the response shape you need.
Check behaviour: assert on what the user would see or what was called, not on internals.
“For the timer, I don't want a test that really waits 30 seconds, so I turn on fake timers. Then I advance the clock to just before 30 seconds and check the warning hasn't fired, advance one more millisecond and check it has. That also proves the boundary. For fetch, I replace it with a mock that resolves to an object with ok and a json method, so I can test the success path, a 500 and a thrown network error without a server. In a real codebase I'd rather mock our own API module than fetch itself, because then the tests don't care how the request is built. And I reset mocks and restore real timers after each test, so one test can't leak into the next.”
afterEach(() => {
jest.useRealTimers();
jest.restoreAllMocks();
});
test('warns after 30 seconds idle', () => {
jest.useFakeTimers();
const onIdle = jest.fn();
startIdleTimer(onIdle, 30000);
jest.advanceTimersByTime(29999);
expect(onIdle).not.toHaveBeenCalled();
jest.advanceTimersByTime(1);
expect(onIdle).toHaveBeenCalledTimes(1);
});
test('loads the user name', async () => {
jest.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: true,
json: async () => ({ name: 'Asha' }),
});
await expect(loadUserName(7)).resolves.toBe('Asha');
});
Using real waits in tests, or tests that call the real API and fail whenever it is slow.
Suspect shared state: module-level caches, mocks that were never restored, fake timers left on, leftover DOM.
Narrow it down: run it with only the test before it, then halve the list until you find the one that breaks it.
Fix the leak: reset in afterEach and make each test set up what it needs.
“If it passes alone and fails with its neighbours, some earlier test is leaving something behind. The usual suspects are a module-level cache or singleton that keeps data between tests, a mock that was set up but never restored, fake timers still switched on, or DOM nodes left in the test document. To find the culprit, I run the failing test together with just one earlier test at a time, or halve the file until it passes again. Last time it was a module that cached the logged-in user: one test logged in as an admin, and a later test expected a guest. The fix was a reset function for that cache called in beforeEach, plus restoring mocks after every test in the config. I avoid adding retries, because that hides the leak.”
Marking the test as skipped or adding automatic retries and moving on.
Network first: check the request and response, so you know whether it is a front-end or back-end problem.
Breakpoints: conditional breakpoints, logpoints and pausing on exceptions, then read the call stack and scope.
DOM and events: break on DOM changes and on event listeners when you don't know which code is touching an element.
“I start in the Network tab, because half the time the request itself is wrong or the response isn't what the code expects, and that tells me which side the bug is on. If the data is fine, I set a breakpoint in the Sources panel where the value first looks wrong and read the call stack and the scope, so I see how we got there. In a loop I use a conditional breakpoint, like only when the id matches the broken row, and logpoints when I want a log without editing code. Pause on exceptions is great when something is caught and swallowed. And when I don't know what's changing an element, I set a breakpoint on DOM subtree modifications, and it stops on the exact line that touched it.”
Only ever using console.log and page reloads, and never having used a breakpoint.
Catch with a purpose: handle it, add context and rethrow, or let it go up.
Keep the chain: pass the original as cause, and use small Error subclasses for errors callers need to tell apart.
At the edge: one place shows a friendly message and logs the full error.
“My rule is to catch only where I can do something: recover, add context, or show the user a message. In between, I let errors go up. When I add context I don't throw away the original. I create a new Error with a clear message and pass the original as cause, so the log shows 'Could not load invoice 42' and underneath it the real network error. For errors callers need to react to, like not found or a validation failure, I use small subclasses with their own name, so the caller can check instanceof. Then at the edge, the route handler or the top of the screen, one piece of code decides what the user sees and sends the whole chain to our error logging. What I avoid is catch blocks that just log and carry on.”
class NotFoundError extends Error {
constructor(message, options) {
super(message, options);
this.name = 'NotFoundError';
}
}
async function loadInvoice(id) {
try {
return await getJson('/api/invoices/' + id);
} catch (err) {
if (err.status === 404) throw new NotFoundError('No invoice ' + id, { cause: err });
throw new Error('Could not load invoice ' + id, { cause: err });
}
}
Catch blocks everywhere that log and continue, or rethrowing a new error with the original thrown away.
Risk: the text came from a user, and markup like an image with an onerror attribute runs script when inserted.
Fix: textContent or creating elements, so the text is never parsed as HTML.
If HTML is needed: a proven sanitiser with an allow list, plus a Content Security Policy as a second layer.
“I'd say the API being ours doesn't matter, because the text inside it was typed by a user. If someone posts a comment with an image tag whose onerror attribute runs code, innerHTML parses it and that code runs in every reader's session, with access to whatever the page can reach. A script tag inserted through innerHTML won't run, which is why people think they're safe, but event handler attributes do. The fix is simple: set textContent, or build the elements yourself, so the comment is always treated as text. If we really need formatting like bold and links, we run it through a well-tested sanitiser with a short allow list of tags, rather than writing our own regex. And a Content Security Policy that blocks inline scripts gives us a second layer.”
const p = document.createElement('p');
p.textContent = comment.text; // shown as text, never parsed as HTML
list.append(p);
Agreeing it's safe because the data comes from our own server, or trying to block script tags with a regex.
Client: listen to the form's submit event, set an in-flight flag and disable the button until the request settles.
Recover: re-enable it in finally, so an error doesn't leave the user stuck.
Server: an idempotency key or unique constraint, because a slow network, a retry or a second tab still gets through.
“On the front end I handle the form's submit event, not the button's click, because pressing Enter also submits. When a submit starts I set a flag and disable the button, and any submit while the flag is set is ignored. I clear it in a finally block, so if the request fails the user can try again instead of staring at a dead button. But that only stops the easy case. A user can double-submit from two tabs, the browser can retry, or our own retry logic can resend after a timeout. So I also generate an idempotency key when the checkout screen opens and send it with the request, and the back end refuses a second order with the same key. The front-end guard is for a nice experience; the server check is the real protection.”
let submitting = false;
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (submitting) return;
submitting = true;
button.disabled = true;
try {
await placeOrder(readForm(form), idempotencyKey);
} finally {
submitting = false;
button.disabled = false;
}
});
Only disabling the button on click and calling it fixed, with no thought for the server side.
Problem: scroll fires very often, and reading positions on each one forces layout work on the main thread.
Rebuild: an IntersectionObserver on a small marker at the end of the list, with a root margin to load early.
Traps: block overlapping loads, stop observing at the end, and handle the marker staying visible after a short page.
“The old code listened to scroll, and on every event it measured the list with getBoundingClientRect to see if we were near the bottom. Scroll fires constantly and each measurement can force layout, so it stuttered on slower phones. I replaced it with an IntersectionObserver watching an empty marker element after the last item. The browser tells us when the marker comes into view, and a root margin makes that happen a little before the user actually reaches the end, so the next page is ready. I kept a loading flag so we never fetch the same page twice, and stopped observing once the API said there were no more pages. One trap I hit: if a page is short and the marker stays visible, the observer doesn't fire again, so after each load I stop observing the marker and observe it again, which makes the browser check it once more.”
let loading = false;
const observer = new IntersectionObserver(async ([entry]) => {
if (!entry.isIntersecting || loading) return;
loading = true;
try {
const hasMore = await loadNextPage();
observer.unobserve(marker);
if (hasMore) observer.observe(marker); // checks again, fires if still in view
} finally {
loading = false;
}
}, { rootMargin: '400px' });
observer.observe(marker);
Keeping the scroll listener and only adding more work to it, or not guarding against the same page loading twice.
Cause: parsing runs on the main thread, and while it runs the page can't paint or respond to input.
Move it off: a Web Worker does the parsing and posts the rows back.
Or break it up: process in chunks and yield between them, showing progress.
“JavaScript on a page runs on one main thread, and that thread also paints the screen and handles clicks. Our parser went through the whole file in one synchronous loop, so for those seconds nothing else could run, not even the spinner animation, which is why it froze. Making it async wouldn't help, because the work itself is still one long block. I moved the parsing into a Web Worker. The page posts the File to the worker, the worker reads and parses it, sends progress messages as it goes, and posts the rows back at the end. The main thread stays free, so the spinner turns and the user can cancel. The one cost is that data sent between them is copied, so for very large results I sent them back in batches.”
// main.js
const worker = new Worker(new URL('./parse-worker.js', import.meta.url), { type: 'module' });
worker.onmessage = (event) => {
if (event.data.type === 'progress') showProgress(event.data.done);
if (event.data.type === 'rows') renderRows(event.data.rows);
};
worker.postMessage(file); // a File can be sent straight to a worker
// parse-worker.js
self.onmessage = async (event) => {
const text = await event.data.text();
const rows = parseCsv(text, (done) => self.postMessage({ type: 'progress', done }));
self.postMessage({ type: 'rows', rows });
};
Saying that wrapping the loop in a promise or an async function would stop the freeze.
Versions: a caret range lets a fresh install pull a newer minor or patch release than the one on your machine.
Lock file: commit it, and install on CI with npm ci, which installs exactly what it lists.
Environment: the same Node version everywhere, pinned in the repo.
“Nobody changed the dependency in package.json, but a range like caret 2.3.0 accepts any later 2.x release, so a fresh install can pull a newer version than the one I have. Or it's a dependency of a dependency, which package.json doesn't even list. The first thing I check is whether the lock file is committed and up to date, and whether CI uses npm install or npm ci. npm ci installs exactly what the lock file says and fails if the lock file and package.json disagree, which is what you want on a build server. I compare the installed versions on both machines to find the one that moved. The other usual cause is a different Node version on CI, so we pinned it in the repo and in the pipeline.”
Deleting node_modules and the lock file until it works, without finding what changed.
Measure: run a bundle analyser to see what actually got added and where it ended up.
Split: load the library with a dynamic import on the page that needs it, so it becomes its own chunk.
Trim: import only what you use, and prefer libraries that ship ES modules the bundler can tree-shake.
“First I'd look rather than guess, so I run the bundle analyser for our build and see the library sitting in the main chunk that every page downloads. Since only the report page uses it, I load it with a dynamic import when that page opens. The bundler then puts it in a separate chunk, the home page never downloads it, and the report page shows a small loading state for a moment. I also check how we import it. Importing the whole package can pull in every chart type, while importing only the chart we use lets the bundler drop the rest, as long as the library ships ES modules without side effects. Last time that brought the main bundle back close to where it was, and I added a size check to CI so we'd notice next time.”
async function openReport() {
const { renderSalesChart } = await import('./sales-chart.js'); // separate chunk
renderSalesChart(document.querySelector('#chart'), reportData);
}
Not knowing how to see what is inside the bundle, or accepting a slower home page for one internal report.
Start: what the ticket asked, and what you clarified before building.
Build: the choices you made yourself, and how you tested them.
Ship: how it went out, what you watched afterwards and what you'd do differently.
“At my last company I owned autosave for a long application form. The ticket just said 'save progress', so first I asked the product owner what happens with two open tabs and whether a half-filled invalid field should save. We agreed the latest save wins and we save drafts even if invalid. I built it so changes save a moment after the user stops typing, with a small 'Saved' or 'Offline, will retry' label. The choices I made myself were saving only the changed fields to keep requests small, and queueing saves while offline. I wrote unit tests for the queue and a browser test for the full flow, and we released it behind a feature flag to a small group first. Watching errors that week, I found a bug on slow networks where an older save finished after a newer one, and fixed it by sending a version number.”
Describing only the code you typed, with no questions asked, no testing and no idea what happened after release.
The feedback: what the reviewer pointed out, in your own code.
Why it mattered: the bug or cost it would have caused.
The habit: what you do differently now, and whether you passed it on.
“Early on I wrote a helper that took the list of orders from our state and called sort on it before returning it. A senior reviewer asked me what else was holding that array. I hadn't thought about it: sort works in place, so my helper was quietly reordering the same list another part of the page was showing, and that explained a bug we'd seen where a table jumped around. Her point was that a function shouldn't change what it's given unless that's its whole job. Since then I treat inputs as read-only. I use toSorted, spread copies, and map instead of changing things in place, and when I do mutate on purpose, I name the function so it's obvious. I now leave that same comment on other people's pull requests when I spot it.”
Saying you never got useful feedback, or describing a comment you accepted without understanding why.
The task: what you estimated and how far off it was.
What you missed: the specific unknowns, not just 'it was harder than expected'.
Now: how you break work down, flag risk and speak up early.
“I once said two days for adding a file upload with a preview to our profile page. It took more than a week. The upload itself was quick. What I hadn't counted was that photos from phones came in huge and sometimes rotated, so I needed resizing and orientation handling in the browser, plus a progress bar, a cancel option and proper error messages when the upload failed halfway. Then testing on real phones turned up two more issues. I also waited too long to tell my lead, hoping I'd catch up. Now I split a task into the happy path and the edge cases, estimate each one, and do a small spike first when something is new to me. And I raise it the day I see an estimate slipping, not at the deadline.”
Blaming the requirements or other people entirely, with nothing you would do differently.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your live interview audio and screen are never stored. Your resume and notes are saved to your account so the app fills them in on any computer. It stays out of screen share on every plan, including Free; only you can see it.