Skip to content
>_ StartupJobList_

Interview questions · Worked answers

JavaScript Interview Questions: 10 Worked Answers

Prepare by tracing what code retains, when callbacks run, and how runtime values cross trust boundaries. The examples focus on failures that change observable behavior.

Intermediate JavaScript developers · Modern JavaScript language behavior across browser and Node hosts. Host-specific scheduling, DOM security and performance details are named where they matter.

What does a closure keep alive?

A closure is a function together with access to the lexical environment in which it was created. It retains access to bindings, not a frozen copy of each value. This lets a callback use state after the outer function returns, but it also means a long-lived listener can keep reachable objects alive.

Each call to makeCounter creates a distinct binding. The returned function can still update it. A closure is not automatically a leak; the practical question is whether a long-lived root such as a DOM listener still references data that should have been released. Remove listeners or cancel timers when their owning view/lifecycle ends.

JS · Worked example
function makeCounter() {
  let count = 0;
  return () => ++count;
}
const next = makeCounter();
next(); // 1
next(); // 2

makeCounter() is called once, so next closes over one count binding. Its first call increments that binding to 1; its second call increments the same binding to 2. Calling makeCounter() again would create an independent counter.

Executed with Node.js v24.14.0.

Follow-up: Does copying count into another variable make a private snapshot?

Only if that copy is a primitive value at that time. An object copied into another binding still refers to the same object unless explicitly cloned.

Read the source: MDN Closures · MDN let

↑ Back to questions

Why are two equal-looking objects unequal?

Objects compare by identity. Two separately created objects with the same fields are different references, while two variables that point to the same object compare equal. Mutation through either alias changes the shared object.

Object spread made a shallow copy. It copied the user reference. This matters when updating state, caching objects by identity, and writing tests that accidentally share nested fixtures. A deep clone is not always safe or appropriate: it may lose prototypes, functions, or special values. Choose a data model and clone operation for the values actually supported.

JS · Worked example
const a = { user: { name: 'Ada' } };
const b = { ...a };
console.log(a === b);             // false
console.log(a.user === b.user);   // true
b.user.name = 'Lin';
console.log(a.user.name);         // 'Lin'

b !== a because object spread creates a new outer object, while b.user === a.user because the nested object reference is shared. Changing b.user.name to Lin therefore makes a.user.name read Lin too.

Executed with Node.js v24.14.0.

Follow-up: Should I use JSON.stringify to compare objects?

Not as a general equality function. Property order/unsupported values and serialization semantics make that a different operation. Compare the fields that define domain equality or use a deliberate value-equality implementation.

Read the source: MDN equality comparisons · MDN structured clone

↑ Back to questions

What order do synchronous code and promise callbacks run?

A synchronous job runs to completion before queued promise reactions run. A timer callback is scheduled through the host environment and is not a precise deadline. Exact ordering between host tasks can depend on whether code runs in a browser or Node and where it is scheduled; explain the context before predicting a trace.

The synchronous output is A, then D; the fulfilled promise reaction runs after the current synchronous job. The timer runs in a later host task. The key distinction is “after this stack/job” versus “at an exact millisecond.” Do not generalize one host's full task-queue ordering to all JavaScript environments.

JS · Worked example
console.log('A');
Promise.resolve().then(() => console.log('B'));
setTimeout(() => console.log('C'), 0);
console.log('D');

The logged order is A, D, then B; C is from a later timer task and is not an exact-deadline guarantee. A busy current job delays both queued reactions and timers.

Executed with Node.js v24.14.0.

Follow-up: Does await block the JavaScript thread?

It suspends the async function and schedules its continuation when the awaited promise settles; synchronous code outside that function can continue. Synchronous work after the await still runs on its host thread when resumed.

Read the source: MDN execution model · MDN promises · MDN setTimeout()

↑ Back to questions

Why can two correct async requests still publish stale data?

Starting request B after request A does not guarantee B finishes last. If A resolves later, it can overwrite the newer result unless the caller cancels or rejects stale publication. Cancellation is useful but not every operation can be stopped after it starts, so keep a correctness guard.

This generation check permits only the newest started request to publish. In a browser, an AbortController can also cancel a fetch; still handle rejection and stale completion. If requests are intentionally independent, such as saving separate documents, a single global generation would be wrong. The policy must match the user action.

JS · Worked example
let latest = 0;
async function search(query) {
  const requestId = ++latest;
  const result = await fetchResults(query);
  if (requestId !== latest) return;
  renderResults(result);
}

If request A starts first and finishes after B, A is ignored because its captured generation is no longer current. The guard prevents stale publication; aborting A can save work but does not replace the guard for APIs that finish despite cancellation.

Illustrative, not executed.

Follow-up: Is aborting enough?

No. Some operations cannot be aborted, and a response may win a race with cancellation. Prevent stale state writes explicitly when stale results would be harmful.

Read the source: MDN async functions · MDN AbortController

↑ Back to questions

Does `Promise.all` cancel the other work when one promise rejects?

No. Promise.all rejects when an input rejects, but it does not automatically cancel the remaining operations. Other work can continue and produce side effects after the caller has handled the failure.

For independent reads, fail-fast aggregation may be right. For parallel writes, ask whether partial success is allowed and how it is recorded. If every outcome matters, Promise.allSettled returns a result for each input. If operations support cancellation, use a shared abort signal; if they mutate durable state, design idempotency or compensation separately. A promise combinator is not a transaction manager.

Follow-up: Should I always use allSettled?

No. It changes error policy by waiting for all outcomes. Use it when the caller must inspect every result, not just to hide failure.

Read the source: MDN Promise.all · MDN Promise.allSettled

↑ Back to questions

How should external data be checked?

Treat JSON, query strings, storage and message events as runtime values that can be missing or malformed. A type-like comment or a cast does not validate them. Check the shape before using it, distinguish absent from null if the contract does, and avoid coercing data silently.

This is a deliberately small schema. Production code should validate every required field and define unknown-field behavior. typeof null === 'object' is a language quirk, so the explicit null check matters. Validation should occur at the trust boundary, then internal functions can use the parsed representation.

JS · Worked example
function parseUser(value) {
  if (value === null || typeof value !== 'object' || Array.isArray(value)) {
    throw new TypeError('user must be an object');
  }
  if (typeof value.id !== 'string') throw new TypeError('id must be a string');
  return { id: value.id };
}

For the shown {id: string} contract, null, an array, a missing id, or a numeric id is rejected before the value reaches application code. The explicit null check is needed because typeof null is "object".

Executed with Node.js v24.14.0.

Follow-up: Should unknown fields cause rejection?

Follow the API contract. Ignoring extra fields can support forward compatibility; rejecting them can catch mistakes in a closed format. Make that policy explicit.

Read the source: MDN typeof · MDN strict equality

↑ Back to questions

What do ES modules change about scope and loading?

ES modules have module scope and explicit imports/exports. An imported binding is live: the importer observes the exported binding's current value, while it cannot assign to the imported name. This differs from copying a value into a local variable at import time. Module loading and cycles can expose an uninitialized binding if evaluation reads it too early.

Keep module initialization simple and avoid circular dependencies for stateful setup. Whether a .js file is treated as ESM depends on host/package configuration; do not infer it from syntax alone in a mixed CommonJS repository.

JS · Worked example
// counter.js
export let count = 0;
export function increment() { count += 1; }

// view.js
import { count, increment } from './counter.js';
increment();
console.log(count); // 1

The import reads the exported binding rather than a copied value, so a later exported assignment is observed. Whether this source parses as ESM depends on the host’s module mode; confirm the package/file configuration before moving it into a mixed CommonJS project.

Illustrative, not executed.

Follow-up: Are imports loaded asynchronously?

Static imports are resolved as part of module loading before the importing module body executes. Dynamic import() returns a promise and is an explicit asynchronous loading operation.

Read the source: MDN JavaScript modules · MDN import

↑ Back to questions

How does unsafe HTML interpolation become an injection bug?

Assigning untrusted text to an HTML parser can turn data into markup and script-capable behavior. For plain text, use a text sink such as textContent; do not build markup by concatenating user input. If rich HTML is a product requirement, use a maintained sanitizer with a narrow policy and still apply browser security headers as defense in depth.

The example avoids HTML parsing; a CSP can reduce some exploit paths but does not make unsafe sinks correct. Validate URLs separately before putting them in navigation attributes. Context matters: HTML text, attributes, URLs and JavaScript strings have different rules.

JS · Worked example
const label = new URLSearchParams(location.search).get('name') ?? '';
output.textContent = label;      // text, not parsed markup
// output.innerHTML = label;     // unsafe for untrusted input

With textContent, a value such as <img src=x onerror=alert(1)> is displayed as text instead of parsed into an element. If the value is intended to be a URL, validate its scheme and destination separately before assigning a navigation property.

Illustrative, not executed.

Follow-up: Is escaping every string once sufficient?

No. Encoding must match the destination context. Prefer APIs that treat data as data, and sanitize only when the feature intentionally accepts markup.

Read the source: MDN CSP · MDN Element.innerHTML · MDN Node.textContent

↑ Back to questions

How do you test a timer or asynchronous branch without flaky sleeps?

Test a promise by awaiting the returned promise and asserting its outcome. If time itself is part of the contract, control timers with the selected test framework's fake clock and advance it deliberately. For an out-of-order race, use manually controlled deferred promises so the test chooses which request resolves first. A sleep checks elapsed wall time, not the intended causal behavior.

A race test should start A, start B, resolve B, then resolve A, and assert that B remains visible. A separate test can assert that cancellation was requested. Do not assert a private sequence of callbacks unless that order is part of the externally observable contract.

Follow-up: Do fake timers test real event-loop behavior?

No. They test code's interaction with the timer API under a controlled scheduler. Keep an integration test for host behavior when that matters.

Read the source: Node.js Test Runner · MDN Promises

↑ Back to questions

What does a good performance claim require?

Name the operation, input size, runtime and environment, then measure representative behavior before changing code. A short callback can still be expensive when input size grows; asynchronous I/O does not make CPU-bound JavaScript free. In Node, long callbacks can block the event loop; in browsers, long main-thread tasks harm interaction responsiveness. Profile first, then test one change against the same workload.

A useful result records the same function, input size and runtime before and after, plus repeated timings and the relevant host metric. If the benchmark is a browser interaction, include long-task or responsiveness evidence; if it is Node, inspect event-loop delay and CPU stacks. An unmeasured rewrite supplies no performance result.

Follow-up: Should a loop always be rewritten with map or a worker?

No. A worker adds messaging, copying/shared-memory and lifecycle costs. Change the design when measurements show the loop is the bottleneck and the work can be safely isolated.

Read the source: Node: Don't block the event loop · MDN execution model

↑ Back to questions

Salary context

Advertised salaries for these skills

These are Hacker News job posts that mention these technologies, not employee earnings or a measure of what a skill adds to pay. The amounts summarize annualized advertised minimums in approximate USD. Groups can overlap and differ in roles, locations and dates.

JavaScript posts

79 posts · 79 usable advertised salary amounts

2024-07-01 to 2026-10-03

$130,000

Median advertised minimum, approximate USD/year

Middle 50%: $100,000–$150,000

Explore JavaScript salaries → Browse matching job posts →
Currency and calculation basis

Approximate USD using ECB daily reference rates as of 2026-10-09. Original currencies and offer ranges remain on the source listings. These figures cover posts tagged JavaScript, rather than every job in the broader technology family.

↑ Back to questions

Keep exploring