Interview Questions In Javascript With Answers

8 min read

JavaScript remains the backbone of modern web development, powering everything from simple interactive buttons to complex single-page applications and server-side environments like Node.js. Whether you are a fresh graduate stepping into the tech industry or a seasoned developer aiming for a senior role, mastering the core concepts of the language is non-negotiable. Technical interviews for JavaScript roles rarely focus solely on syntax; they probe your understanding of the engine under the hood—how the event loop handles asynchronous code, how closures capture scope, and how the prototype chain enables inheritance. This guide walks through the most critical interview questions categorized by concept, providing clear answers and the "why" behind them so you can articulate your knowledge with confidence.

The Foundations: Scope, Hoisting, and Closures

Interviewers almost always start with the fundamentals because they dictate how every other piece of code behaves. A shaky grasp here leads to bugs that are notoriously difficult to debug in production.

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

It's the quintessential warm-up question. The answer revolves around scope, hoisting, and reassignment.

  • var: Function-scoped or globally scoped. It is hoisted to the top of its scope and initialized with undefined. This means you can reference a var variable before its declaration (though it will be undefined). It can be re-declared and updated.
  • let: Block-scoped (bound by {} curly braces). It is hoisted but stays in the Temporal Dead Zone (TDZ) until the declaration line is reached. Accessing it before declaration throws a ReferenceError. It can be updated but not re-declared in the same scope.
  • const: Also block-scoped and subject to TDZ. It must be initialized at declaration. It cannot be updated or re-declared. Crucial nuance: For objects and arrays declared with const, the binding is immutable, not the value. You can still mutate properties (const obj = {}; obj.prop = 1; works).

Interview Tip: Always mention the Temporal Dead Zone when discussing let and const. It signals you understand the specification, not just the behavior No workaround needed..

Explain Hoisting in JavaScript.

Hoisting is JavaScript's default behavior of moving declarations to the top of the current scope (script or function) before code execution.

  • Variable Hoisting: var declarations are hoisted and initialized with undefined. let and const are hoisted but not initialized (TDZ). Function declarations are hoisted completely (name and body), allowing you to call a function before it appears in the code.
  • Function Expressions / Arrow Functions: These are not hoisted fully. Only the variable declaration (if var) is hoisted. The assignment (the function body) stays in place. Calling them early results in TypeError: myFunc is not a function (for var) or ReferenceError (for let/const).

What is a Closure? Give a practical example.

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In simpler terms, a closure gives you access to an outer function’s scope from an inner function, even after the outer function has returned Still holds up..

Practical Use Case: Data Encapsulation / Module Pattern.

function createCounter() {
  let count = 0; // Private variable
  return {
    increment: () => ++count,
    decrement: () => --count,
    getCount: () => count
  };
}

const counter = createCounter();
console.log(counter.Worth adding: increment()); // 1
console. log(counter.

Here, `increment`, `decrement`, and `getCount` form closures over the `count` variable. The variable persists in memory because the inner functions hold a reference to it, effectively creating *private state* in a language that historically lacked private class fields.

## Asynchronous JavaScript: The Event Loop and Promises

This is where junior and mid-level developers often separate. Understanding the **Event Loop** is mandatory for writing performant, non-blocking applications.

### Explain the Event Loop: Call Stack, Web APIs, Callback Queue, Microtask Queue.

JavaScript is single-threaded. The Event Loop is the mechanism that allows it to handle non-blocking operations (like `setTimeout`, `fetch`, DOM events) by offloading them to the browser (Web APIs) and scheduling their callbacks.

1.  **Call Stack**: Where synchronous code executes (LIFO).
2.  **Web APIs**: Browser-provided APIs (`setTimeout`, `fetch`, `DOM`). When an async task completes, its callback is pushed to a **Task Queue** (Macrotask Queue) or **Microtask Queue**.
3.  **Microtask Queue**: Holds Promise callbacks (`.then`, `.catch`, `.finally`), `queueMicrotask`, `MutationObserver`. **High Priority.**
4.  **Macrotask Queue (Callback Queue)**: Holds `setTimeout`, `setInterval`, `setImmediate`, I/O, UI Rendering. **Lower Priority.**
5.  **The Loop**: The Event Loop checks: *Is the Call Stack empty?* If yes, it checks the **Microtask Queue** first. It drains *all* microtasks (executing them one by one, potentially adding more microtasks) before touching a single macrotask. Then it takes one macrotask, executes it, renders UI if needed, and repeats.

**Classic Interview Question:** *What is the output?*

```javascript
console.log('1. Script Start');

setTimeout(() => console.log('2. setTimeout'), 0);

Promise.resolve().then(() => console.log('3. Promise 1'));

console.log('4. Script End');

Output: 1 -> 4 -> 3 -> 2. Why? Synchronous code runs first. setTimeout goes to Macrotask Queue. Promise.then goes to Microtask Queue. After script ends (stack empty), Microtask queue drains (3), then Macrotask runs (2).

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

Handling multiple concurrent promises is a daily task.

  • Promise.all(iterable): Fail-fast. Resolves when all promises resolve. Rejects immediately if any promise rejects. Returns array of results in order. Use when you need all data to proceed (e.g., fetching user profile + permissions simultaneously).
  • Promise.allSettled(iterable): Wait-for-all. Never rejects (unless the iterable itself is invalid). Resolves when all promises settle (either fulfill or reject). Returns array of objects {status: 'fulfilled', value} or {status: 'rejected', reason}. Use for independent tasks where you want results regardless of individual failures (e.g., analytics tracking, multiple non-critical API calls).
  • Promise.race(iterable): First-settled. Settles (resolves or rejects) as soon as any promise settles. Mirrors the outcome of the fastest promise. Use for timeouts (race a fetch against a setTimeout promise that rejects).
  • Promise.any(iterable): First-fulfilled. Resolves as soon as any promise fulfills. Ignores rejections. Only rejects (with AggregateError) if all promises reject. Use when you have multiple sources for the same data (e.g., CDN mirrors) and want the first successful response.

Async/Await vs. Promises: Error Handling Nuances.

async/await is syntactic sugar over Promises, making asynchronous code look synchronous

but it introduces distinct error handling behaviors that frequently trip up developers Simple, but easy to overlook..

The try/catch Imperative

With raw Promises, you attach a .catch() handler. With async/await, you must wrap await calls in try/catch blocks to prevent unhandled promise rejections from crashing your Node process or breaking the React component tree Worth keeping that in mind..

// ❌ Dangerous: Unhandled rejection if fetchUser fails
async function getUserData() {
  const user = await fetchUser(id); // If this rejects, it bubbles up unhandled
  return user;
}

// ✅ Safe: Explicit error boundary
async function getUserData() {
  try {
    const user = await fetchUser(id);
    return user;
  } catch (error) {
    // Handle specific error types (network, 404, validation)
    logger.error('User fetch failed', { id, error });
    throw new AppError('USER_NOT_FOUND', { cause: error });
  }
}

Sequential vs. Parallel Execution: The Performance Trap

The synchronous look of await encourages sequential code, which is often an accidental performance bottleneck That's the whole idea..

// 🐢 SLOW: Sequential (Waits for user, THEN waits for posts)
async function slowDashboard(userId) {
  const user = await fetchUser(userId);      // 300ms
  const posts = await fetchPosts(userId);    // 200ms
  // Total: ~500ms
  return { user, posts };
}

// 🚀 FAST: Parallel (Both start immediately)
async function fastDashboard(userId) {
  // Initiate both promises *before* awaiting
  const [user, posts] = await Promise.But all` (or `Promise. In real terms, all([
    fetchUser(userId),
    fetchPosts(userId)
  ]);
  // Total: ~300ms (max of the two)
  return { user, posts };
}

Rule of thumb: If operations are independent, Promise. allSettled) with a single await is almost always the correct pattern.

The "Floating Promise" Anti-Pattern

Forgetting await inside an async function returns a Promise instead of the resolved value, leading to subtle bugs where code executes out of order or errors are swallowed.

async function notifyUser(userId) {
  // ❌ Missing await: sendEmail returns a Promise, function returns immediately
  sendEmail(userId, 'Welcome!'); 
  logActivity('Email sent'); // Runs BEFORE email actually sends
  
  // ✅ Correct
  await sendEmail(userId, 'Welcome!'); 
  logActivity('Email sent'); // Runs AFTER confirmation
}

ESLint rule require-await (or TypeScript's no-floating-promises) catches this automatically—enable it And that's really what it comes down to..

Error Stack Traces & Context

async/await generally preserves cleaner, more readable stack traces compared to chained .then().catch(). That said, re-throwing errors (throw error) inside catch blocks can sometimes strip context if not careful. Use throw new CustomError('Context', { cause: error }) (ES2022 Error cause option) to maintain the full causal chain for debugging Worth keeping that in mind..


Summary Cheatsheet

Scenario Tool / Pattern Key Reason
All must succeed `Promise.
Independent parallel await Promise.allSettled Inspect individual outcomes, never throws.
First success only `Promise.
Sequential dependency await step-by-step Step B needs data from Step A. Now,
All must finish `Promise.
First winner (any outcome) `Promise.Consider this:
Fire-and-forget (bg tasks) void promise. Consider this: all Fail-fast, ordered results. all([...Practically speaking, any`

The official docs gloss over this. That's a mistake.


Conclusion

Mastering JavaScript concurrency isn't about memorizing queue names—it's about intentional architecture. The Event Loop dictates when code runs; the Promise static methods dictate how multiple asynchronous operations relate to one another; and async/await dictates how readable and maintainable that logic remains.

The senior developer doesn't just ask "Does it work?Worth adding: " They ask: "Does this run in parallel or sequence? Because of that, what happens if this specific promise rejects? Still, is the error actionable? " By internalizing the Microtask/Macrotask priority, choosing the correct Promise.* combinator, and respecting the try/catch discipline of async/await, you transform asynchronous chaos into predictable, resilient, and performant systems.

Currently Live

Trending Now

Neighboring Topics

Other Perspectives

Thank you for reading about Interview Questions In Javascript With Answers. We hope the information has been useful. Feel free to contact us if you have any questions. See you next time — don't forget to bookmark!
⌂ Back to Home