Skip to content
spot_the_ai_bug

JavaScript find-the-bug interview questions

JavaScript bug-hunt questions test whether you know the language's defaults: string sorting, falsy values, zero-based indexes, and what actually ships to the browser. Every snippet below compiles and looks reasonable. Your job is to find the one line that breaks in production.

JavaScript gotchas interviewers love

  • Array.prototype.sort() compares strings unless you pass a comparator.
  • 0, "" and NaN are falsy; use == null to test for missing values.
  • The last index is length - 1 (or use .at(-1)).
  • Every listener you add needs a matching removeEventListener with the same function.
  • Anything in front-end code, including API keys, is public.

5 JavaScript spot-the-bug questions

Each snippet has exactly one bug. Find the line before you open the answer.

Getting the last element

latestMessage() should return the newest message in the chat. Why is the preview always empty?

function latestMessage(messages) {  if (messages.length === 0) return null;  const last = messages[messages.length];  return last.text;}
Show answer

line 3 · Off-by-one

Arrays are zero-indexed, so the last element is at length - 1. messages[messages.length] is undefined, and reading .text on it throws a TypeError.

Fix
  const last = messages[messages.length - 1]; // or messages.at(-1)

Interview follow-up: Bonus points for mentioning Array.prototype.at(-1).

Falsy zero in a stock label

formatStock() renders the stock badge on product cards. What does a sold-out product show?

function formatStock(count) {  if (!count) return "Stock unknown";  if (count < 5) return `Only ${count} left`;  return "In stock";} formatStock(0); // expected a "Sold out" badge
Show answer

line 2 · Wrong edge case

!count is true for 0 as well as for null and undefined, so a sold-out product shows "Stock unknown" instead of being treated as zero stock.

Fix
  if (count == null) return "Stock unknown";  if (count === 0) return "Sold out";

Interview follow-up: Know the falsy values cold: false, 0, -0, 0n, "", null, undefined, NaN.

Scroll listener per page view

trackScrollDepth() runs on every client-side route change in a single-page app. What goes wrong after a few minutes of browsing?

export function trackScrollDepth(pageId) {  let maxDepth = 0;  window.addEventListener("scroll", () => {    const depth = (window.scrollY + window.innerHeight) / document.body.scrollHeight;    maxDepth = Math.max(maxDepth, depth);  });  return () => sendBeacon("/analytics", { pageId, maxDepth });}
Show answer

line 3 · Resource leak

A new scroll listener is added on every navigation and never removed. Old listeners keep running (and keep their closures alive), so each scroll does more and more work and memory grows for the whole session.

Fix
  const onScroll = () => {    const depth = (window.scrollY + window.innerHeight) / document.body.scrollHeight;    maxDepth = Math.max(maxDepth, depth);  };  window.addEventListener("scroll", onScroll, { passive: true });  return () => {    window.removeEventListener("scroll", onScroll);    sendBeacon("/analytics", { pageId, maxDepth });  };

Interview follow-up: Removing a listener requires the same function reference that was added, so name the handler.

Sorting numbers with sort()

topScores() should return the three highest scores. What does it return for [100, 25, 9, 1000]?

function topScores(scores) {  return [...scores].sort().reverse().slice(0, 3);}
Show answer

line 2 · Wrong API usage

Without a comparator, sort() converts values to strings and sorts them lexicographically: [100, 1000, 25, 9]. Reversed, that gives [9, 25, 1000], and 100 is dropped.

Fix
  return [...scores].sort((a, b) => b - a).slice(0, 3);

Interview follow-up: Mention that copying first ([...scores]) matters because sort() mutates in place, or use toSorted().

Secret key in front-end code

This runs in the browser when the user clicks "Pay". What's the problem?

const STRIPE_SECRET_KEY = "sk_live_51Hx9EXAMPLEf3kL0rEmIpSuM"; export async function createCharge(amountCents, token) {  const res = await fetch("https://api.stripe.com/v1/charges", {    method: "POST",    headers: { Authorization: `Bearer ${STRIPE_SECRET_KEY}` },    body: new URLSearchParams({ amount: amountCents, currency: "usd", source: token }),  });  return res.json();}
Show answer

line 1 · Leaked secret

Everything shipped to the browser is public. Anyone can read the live secret key from the bundle or the network tab and use it to issue refunds, read customers, or create charges. Secret keys belong on the server; the browser should call your own endpoint.

Fix
// Browser: call your backend, which holds the secret key.export async function createCharge(amountCents, token) {  const res = await fetch("/api/charges", {    method: "POST",    headers: { "Content-Type": "application/json" },    body: JSON.stringify({ amountCents, token }),  });  return res.json();}

Interview follow-up: Distinguish publishable keys (safe in the browser) from secret keys (never).

Every day, everyone gets the same pull request written by AI. It looks clean. It has comments. It has exactly one real bug.

  1. 1

    Find the line

    Tap the buggy line, then hit Submit. You get 3 attempts.

  2. 2

    Read the hints

    A miss within 2 lines is 🔥 Warm. Anything further is ❄️ Cold.

  3. 3

    Name the bug

    Found it? Bonus round: pick the bug type from 4 options. It doesn't affect your win, just your bragging rights.

// your share card

found in 3/3

🟥 cold miss · 🟨 warm miss · 🟩 found · ⬛ unused

Keyboard: ↑ ↓ to move between lines, Enter to submit. A new bug drops every day at midnight IST.

0
Played
0
Win %
0
Streak
0
Max

Attempts distribution