Hiring for this role?
Start free with this planFree for your first open role.
Why Hirezen?
- Every interviewer runs the same script and marks the same signals.
- AI drafts the write-ups, and the debrief puts every read side by side.
- No ATS to set up first, and no bot in the call.
Frontend Developer interview questionsCoding Test round
A 60 min interview plan with a time-boxed script, what each question is for, and the signals to score against. Key skills: A frontend coding test on a search box with planted bugs: why the results race the input despite a debounce, a test that controls which response lands first, loading and error states that cannot contradict the list, and results that work by keyboard and screen reader.
Opening
Who is interviewing, how the round will run, and a question to settle the candidate in. The standard openingRead it before you touch it
What this part is for
Purpose
Runs over a small repository you build beforehand and share at the start, in the candidate's own editor or a hosted sandbox. Build it to a known composition. `src/api.ts` exports `searchProducts(query, { signal })`: each call waits a random 80 to 900 ms, about one call in eight rejects with an `Error` whose message is "503", and the function honours an `AbortSignal` by rejecting with an `AbortError` — correct, and never used. `src/useDebouncedValue.ts` is a 250 ms debounce hook, also correct. `src/SearchBox.tsx` is about sixty lines and holds every plant: an effect on the debounced query that calls `searchProducts` without a signal and writes whatever comes back into one `results` state, in arrival order; a single `loading` boolean set true when any request starts and false when any request succeeds; `.catch(() => {})`, so a failure leaves `loading` true and tells the user nothing; `if (!debounced) return;` at the top of the effect, so clearing the box keeps the old results on screen and a response still in flight lands under an empty input; an input with a placeholder and no label; and results rendered as `div`s with `onClick`, which a keyboard cannot reach and nothing announces. The results use `key={product.id}`, which is right. Add a README with two support reports — results that do not match what was typed, and a spinner that never goes away — and one line saying a debounce went in last sprint and the first report got rarer but did not stop. The starter is React with TypeScript because most teams use it; if yours does not, build the same starter in your framework or in plain DOM, since neither the plants nor the questions depend on React. Book 70 minutes: the candidate's questions come after the 60.
I'm [YOUR_NAME], and I work on the storefront at [COMPANY_NAME]. This is a coding test, but not a puzzle: you get a small component with bugs our support team has reported, and you find and fix them while I watch and ask questions. Use the docs and the browser's devtools the way you normally would.
What this line is for
Purpose
Tells a candidate who prepared for algorithm puzzles that the hour is about a component, so the first minutes go on reading it rather than waiting for a problem statement.
Two reports came in. One: the results sometimes don't match what's in the box. Two: sometimes the spinner never goes away. Someone added a debounce last sprint, and the first report got rarer but didn't stop. Run it, read it, and tell me what's going on before you change anything.
What this line is for
Purpose
Hands over the symptoms and the fix already tried, so the first question scores diagnosis rather than the textbook remedy for a search box.
Explain both reports. For each one, give me the exact order of events that produces it — which request went out when, which response came back when, and what ended up on screen.
What this question is for, and what to listen for
Purpose
Separates candidates who can reason about time in the browser from candidates who hear "search box" and answer "debounce". The debounce is already there and correct, so what they say about it is the first thing to write down.
Signals to score
- Gives a concrete timeline for the wrong results: the request for "la" is still in flight when the request for "lamp" goes out, and the older response lands last and overwrites the newer one
- Explains why the debounce made the report rarer without removing it: the race needs two requests in flight at once, which takes a pause longer than 250 ms mid-typing and a first response slower than the gap
- Says the debounce is correct as written and does not propose a longer delay as the fix
- Finds that one `loading` flag serves every request, so the first success to arrive hides the spinner while the request that matters is still pending
- Traces the endless spinner to the empty `catch`: a failed request never resets `loading` and shows nothing
- Finds the cleared-box path — the old results stay, and a response still in flight lands under an empty input
- Tries the component before theorising — types with a pause, clears the box, forces a failure
- Names the missing label and the click-only results as a separate problem and parks them for later
- Says what is right in the starter — the debounce, the keys, a mock that already accepts a signal — not only what is wrong
Follow-up questions
- The debounce is 250 ms. Why did the wrong-results report get rarer but not stop?
- Type "la", wait half a second, type "mp". Which responses can arrive in which order, and what is on screen after each?
- Two requests are in flight and the older one succeeds first. What does the spinner show?
- Clear the box while a search is still loading. What happens, step by step?
- What in this repository is already right?
Fix the race, and prove it
What this part is for
Purpose
Two questions in this order: a test that fails on the starter, then the fix that makes it pass. If time runs short, the test and the rule that only the current query may write results matter more than a finished error state — ask the candidate to describe the error state rather than build it.
Before you fix anything, write the test that fails because of the race. Then change the component until it passes.
What this line is for
Purpose
Makes the test the first deliverable, so the fix is measured against something that failed rather than against the candidate's confidence.
Write a test that fails on this component because an older response lands after a newer one. It has to fail on every run, not just sometimes.
What this question is for, and what to listen for
Purpose
Reads whether the candidate controls time in a test or races it. A test built on real timers and the mock's random latency passes most runs against the broken component and proves nothing.
Signals to score
- Writes the test before changing the component
- Replaces the API with calls that return promises the test settles by hand, so the test decides which response lands first
- Uses fake timers to step past the 250 ms debounce instead of waiting in real time
- Types "la", advances past the debounce, types "mp", advances again, then resolves "lamp" before "la"
- Asserts on the result names the user sees, not on component state or on how many times the API was called
- Runs it against the starter and reads the failure — the "la" results on screen — before touching the component
- Adds a case for the cleared box: a response that arrives after the input is emptied must not fill the list
- Keeps the test independent of how fast the machine running it is
Follow-up questions
- How does your test decide which response arrives first?
- Does this test wait 250 ms of real time? Should it?
- It passes. Show me it failing on the starter.
- What does the assertion check — what the user sees, or what the component did?
- What about a response that arrives after the box has been cleared?
Now make the test pass. Only the query in the box may decide what the list shows — including when the box is empty — and the spinner, the list and any error must never disagree.
What this question is for, and what to listen for
Purpose
Scored on one rule and on how the fix fails. Most candidates reach for AbortController; the separator is whether the rule is also enforced at the moment results are written, and whether the fix survives the cleared box.
Signals to score
- Makes only the current query able to write results — a check at commit, effect cleanup, or both — and says which part guarantees it
- Passes a signal to `searchProducts` and aborts the superseded request, and knows aborting alone does not stop a response that is already being handled
- Does not show an aborted request as an error
- Replaces the separate loading flag and results with one status for the current query — idle, loading, results, failed — so the spinner and the list cannot disagree
- Stops the spinner on failure and shows an error the user can act on, with a retry
- Clears the results when the box is emptied, and makes sure a request still in flight cannot refill it
- Decides what stays visible while the next results load, and gives the reason
- If reaching for a data-fetching library, says what it does that fixes this rather than only naming it
- Takes the earlier test to green, with the cleared-box case
Follow-up questions
- You abort the old request. Is that enough without any other check?
- The user clears the box while "lamp" is still loading. Walk me through your code.
- A request fails. What does the user see, and what can they do next?
- Why did you keep, or drop, the previous results while new ones load?
- You reached for a library. What does it do with a response for a query that is no longer in the box?
Take the mouse away
What this part is for
Purpose
Reads accessibility in code. Ask for markup and key handling, not styling; pseudo-code for the key handler is fine if the markup is real. Use the second leading question only after the candidate has chosen a structure, because the choice is part of what is scored.
Make the results usable without a mouse and with a screen reader. Show me the markup and the key handling, and tell me what a screen reader user hears from typing "lamp" to choosing a result.
What this question is for, and what to listen for
Purpose
The decision that separates candidates comes before any ARIA: whether choosing a result opens the product page, in which case a labelled input, a list of links and an announcement may be all it needs, or fills the box, in which case it is a combobox and the pattern has to be built whole or not at all.
Signals to score
- Asks what choosing a result does before choosing markup, and says how the answer changes it
- Gives the input a real label, visible or not, instead of relying on the placeholder
- Replaces the clickable `div`s with native links or buttons, or with options inside a listbox — never a `div` with a role and no key handling
- If arrow keys are wanted, keeps focus in the input and points `aria-activedescendant` at the active option, with `aria-expanded` and `aria-controls` on the input
- Says what Enter, Escape and the arrow keys do, including at either end of the list
- Announces the result count through a polite live region that is in the page from the start and changes when a search settles, not on every keystroke
- Announces a failed search as well as a successful one
- Keeps announcements short — a count, not the list — and does not make them assertive
- Shows the active option to sighted keyboard users as well as to screen readers
- Says how they would check it — the keyboard alone, and a named screen reader with a browser — and knows an automated checker cannot tell whether the keys work
Follow-up questions
- When someone chooses a result, does it open a page or fill the box? Does that change your markup?
- Product wants the arrow keys to move through results while the cursor stays in the box. Where is focus, and how does a screen reader know which result is active?
- The count changes several times while someone types "lamp". What do they hear, and when?
- The search failed. What does a screen reader user hear?
- How would you check this before merging, and with what?
Closing
That's the coding done. What do you want to know about how front-end work happens here? [Answer plainly, and offer one true thing someone taking this job should hear: how bugs like these reach the team, whether anyone tests with a screen reader before release, which component nobody wants to touch.]
What this line is for
Purpose
What a candidate asks after an hour inside a race condition shows what they expect the work to be. The bracketed offer only helps if it is true of your team today.
Frontend Developer interviews — common questions
- Who is this Frontend Developer interview plan for?
- It is written for the interviewer, not the candidate: the hiring manager, engineer or panel member running the Coding Test round for a Frontend Developer role. It gives you a 60 min script to follow in the conversation — 4 questions with what each one is for and the signals to score against — so you are not writing the round from scratch the night before.
- What does the Coding Test round assess?
- This round is focused on: A frontend coding test on a search box with planted bugs: why the results race the input despite a debounce, a test that controls which response lands first, loading and error states that cannot contradict the list, and results that work by keyboard and screen reader. It works through Read it before you touch it, Fix the race, and prove it and Take the mouse away, scoring against 36 observable signals, with follow-up prompts on all 4 questions for going deeper where an answer is thin.
- How is the 60 min split up?
- 60 min on 4 questions. The questions take in Read it before you touch it (14 min), Fix the race, and prove it (28 min) and Take the mouse away (18 min). The timings are there so the round stays on schedule and every candidate gets the same shape of interview — which is what makes two candidates comparable afterwards.
- What other rounds should I run for a Frontend Developer?
A single round does not cover a whole role. The other rounds in this library for a Frontend Developer:
Hiring for this role?
Open this plan in Hirezen and make it a position in one click.
- Every interviewer runs the same script and marks the same signals.
- AI drafts the write-ups, and the debrief puts every read side by side.
- No ATS to set up first, and no bot in the call.
Free for your first open role.