Skip to main content

Command Palette

Search for a command to run...

Javascript Interview - Advanced

Updated
View as Markdown
  1. What is Callback Hell and How to avoid this?
    Callback Hell (also known as the “Pyramid of Doom”) refers to a situation in JavaScript (or other asynchronous programming environments) where multiple nested callbacks make code hard to read, understand, and maintain.

     doSomething(function(result1) {
         doSomethingElse(result1, function(result2) {
             doAnotherThing(result2, function(result3) {
                 doFinalThing(result3, function(result4) {
                     // and so on...
                 });
             });
         });
     });
    

    This kind of code grows horizontally and becomes difficult to manage because: It’s hard to read.

    Error handling becomes messy. Logic is tightly coupled.
    How to Avoid Callback Hell -
    i) Use Named Functions
    Instead of writing anonymous functions inside callbacks, define them separately

     function handleResult1(result1) {
         doSomethingElse(result1, handleResult2);
     }
    
     function handleResult2(result2) {
         doAnotherThing(result2, handleResult3);
     }
    
     function handleResult3(result3) {
         doFinalThing(result3, finalCallback);
     }
    
     doSomething(handleResult1);
    

    ii) Use Promises

    Promises flatten the structure and make the flow easier to follow

     doSomething()
         .then(result1 => doSomethingElse(result1))
         .then(result2 => doAnotherThing(result2))
         .then(result3 => doFinalThing(result3))
         .catch(error => console.error(error));
    

    iii) Use Async/Await
    Even better readability, and it looks synchronous

     async function processAll() {
         try {
             const result1 = await doSomething();
             const result2 = await doSomethingElse(result1);
             const result3 = await doAnotherThing(result2);
             const result4 = await doFinalThing(result3);
         } catch (error) {
             console.error(error);
         }
     }
    
     processAll();
    
  2. What are JavaScript Promises? Explain their states.
    A Promise in JavaScript is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
    It’s like a placeholder for a value that will be known in the future — perfect for dealing with asynchronous tasks like API calls, file loading, timers, etc.

     // Basic Syntax
     const promise = new Promise((resolve, reject) => {
         // async operation
         if (success) {
             resolve("Data received");
         } else {
             reject("Something went wrong");
         }
     });
    

    A Promise has three states
    i) Pending

    Initial state.

    The operation is still ongoing (neither fulfilled nor rejected).

    Example: waiting for an API response.

    ii) Fulfilled

    The operation completed successfully.

    resolve(value) is called.

    The promise returns a result.

    Handled with .then() or await.

    iii) Rejected

    The operation failed.

    reject(error) is called.

    An error is returned.

    Handled with .catch() or a try/catch block in async/await.

     let fetchData = new Promise((resolve, reject) => {
         let success = true;
    
         setTimeout(() => {
             if (success) {
                 resolve("Data loaded");
             } else {
                 reject("Failed to load data");
             }
         }, 2000);
     });
    
     fetchData
         .then(data => console.log("Success:", data))
         .catch(error => console.error("Error:", error));
    

    Once a promise is fulfilled or rejected, it is settled — and can't change its state anymore.

  3. How to handle fetch Data API using Promises?
    The fetch() function is a modern way to make HTTP requests. It returns a Promise that resolves to the Response object representing the result of the request.

     // GET Method using Promise
     fetch('https://jsonplaceholder.typicode.com/posts/1')
         .then(response => {
             if (!response.ok) {
                 throw new Error('Network response was not ok');
             }
             return response.json(); // parse JSON from response
         })
         .then(data => {
             console.log("Fetched data:", data);
         })
         .catch(error => {
             console.error("Fetch error:", error);
         });
    

    fetch(url): Initiates a request and returns a Promise.

    .then(response => ...): Handles the response object.

    response.json(): Converts the response body to JSON (also returns a Promise).

    .then(data => ...): Accesses the actual data.

    .catch(error => ...): Catches any network or parsing errors.
    Always check response.ok -

    This checks if the HTTP status is in the 200–299 range (success). Otherwise, you might think it worked even if it returned a 404 or 500.

    Handling a POST API request using Promises with fetch() is very straightforward — you just need to: Set the method to "POST". Include headers (especially "Content-Type": "application/json"). Pass the request body as a JSON string using JSON.stringify().

     // POST Method using Promise
     const postData = {
         title: 'Hello World',
         body: 'This is a post request example',
         userId: 1
     };
    
     fetch('https://jsonplaceholder.typicode.com/posts', {
         method: 'POST',
         headers: {
             'Content-Type': 'application/json' // Tells server you're sending JSON
         },
         body: JSON.stringify(postData) // Convert JS object to JSON string
     })
     .then(response => {
         if (!response.ok) {
             throw new Error('Network response was not ok');
         }
         return response.json(); // Parse JSON response
     })
     .then(data => {
         console.log("POST successful, response data:", data);
     })
     .catch(error => {
         console.error("POST failed:", error.message);
     });
    
  4. How to handle fetch Data API using Async/Await?
    Using async/await with the fetch() API is a clean and modern way to handle asynchronous data fetching in JavaScript.

     // GET Method using Async/Await
     async function fetchData() {
         try {
             const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
    
             if (!response.ok) {
                 throw new Error(`HTTP error! Status: ${response.status}`);
             }
    
             const data = await response.json();
             console.log("Fetched data:", data);
         } catch (error) {
             console.error("Fetch error:", error.message);
         }
     }
    
     fetchData();
    

    async function: Declares an asynchronous function.

    await fetch(...): Waits for the fetch() Promise to resolve.

    response.ok: Checks if the request was successful.

    await response.json(): Waits for the response to be parsed to JSON.

    try...catch: Catches and handles any errors (like network issues or bad responses).
    Let's handle a POST API using async/await — it’s clean, modern, and easy to read.

     // POST Method using Async/Await
     async function postData() {
         const payload = {
             title: 'Hello World',
             body: 'This is a post request using async/await',
             userId: 1
         };
    
         try {
             const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
                 method: 'POST',
                 headers: {
                     'Content-Type': 'application/json'
                 },
                 body: JSON.stringify(payload)
             });
    
             if (!response.ok) {
                 throw new Error(`HTTP error! Status: ${response.status}`);
             }
    
             const data = await response.json();
             console.log("POST successful:", data);
         } catch (error) {
             console.error("POST request failed:", error.message);
         }
     }
    
     postData();
    

    async function: Declares an asynchronous function.

    await fetch(...): Waits for the HTTP request to complete.

    method: 'POST': Specifies you're sending data.

    headers: Tells the server the content type is JSON.

    body: JSON.stringify(...): Converts the JavaScript object to a JSON string.

    await response.json(): Waits for and parses the JSON response.

    try...catch: Catches and handles errors like network failures or bad responses.

  5. What is the difference between .then() and .catch() in promises?
    .then() and .catch() are essential tools for handling Promises in JavaScript — they help you deal with success and error outcomes of asynchronous operations.
    i) .then() – Handles Success
    .then() is used when a Promise is fulfilled (resolved) successfully. It allows you to access the result of the Promise.

     fetch('https://api.example.com/data')
         .then(response => response.json()) // handles the fulfilled state
         .then(data => console.log("Data:", data)); // chained for further processing
    

    ii) .catch() – Handles Errors

    .catch() is used when a Promise is rejected or if an error is thrown anywhere in the chain before it.

     fetch('https://api.example.com/data')
         .then(response => response.json())
         .then(data => console.log("Data:", data))
         .catch(error => console.error("Error:", error)); // handles errors
    

    Difference between Try and Catch in Promises -

    i) Purpose
    try - Handles success
    catch - Handles errors/failures
    ii) Receives
    try - The resolved value
    catch - The error object
    iii) Return Type
    try - Returns a new Promise
    catch - Also returns a new Promise
    iv) Usage
    try - For continuing flow
    catch - For catching and responding to errors

  6. What are the differences between Promise.all, Promise.race, Promise.any, and Promise.allSettled?
    These four Promise utility methods are powerful for handling multiple asynchronous operations in JavaScript, and each behaves differently based on how the Promises resolve or reject.
    i) Promise.all()
    It waits for all Promises to resolve. If anyone fails, it rejects immediately.

     Promise.all([promise1, promise2, promise3])
         .then(results => console.log("All resolved:", results))
         .catch(error => console.error("One failed:", error));
    

    It returns array of all resolved values after all the promises gets resolved.

    ii) Promise.race()
    It returns the first settled promise (either resolved or rejected).

     Promise.race([promise1, promise2, promise3])
         .then(result => console.log("First settled (resolved):", result))
         .catch(error => console.error("First settled (rejected):", error));
    

    iii) Promise.any()
    It returns the first resolved promise. Ignores rejections unless all fail.

     Promise.any([promise1, promise2, promise3])
         .then(result => console.log("First resolved:", result))
         .catch(error => console.error("All failed:", error));
    

    iv) Promise.allSettled()
    It waits for all Promises to settle, regardless of outcome.

     Promise.allSettled([promise1, promise2, promise3])
         .then(results => {
             results.forEach(result => {
                 if (result.status === "fulfilled") {
                     console.log("Resolved:", result.value);
                 } else {
                     console.log("Rejected:", result.reason);
                 }
             });
         });
    

    Use Case Summary -

    i) Use Promise.all() when all must succeed

    ii) Use Promise.race() when only the fastest matters

    iii) Use Promise.any() when any one success is enough

    iv) Use Promise.allSettled() when you need all results regardless of success/failure

  7. How do you cancel an ongoing promise?
    i) Use AbortController (for fetch requests)

     const controller = new AbortController();
     const signal = controller.signal;
    
     fetch('https://jsonplaceholder.typicode.com/posts', { signal })
         .then(response => response.json())
         .then(data => console.log(data))
         .catch(error => {
             if (error.name === 'AbortError') {
                 console.log('Fetch aborted');
             } else {
                 console.error('Fetch error:', error);
             }
         });
    
     // Cancel the fetch after 2 seconds
     setTimeout(() => controller.abort(), 2000);
    

    ii) Custom Cancelable Promise Wrapper
    You can wrap a Promise to make it cancellable manually, though this only works for operations you control (like timeouts or intervals).

     function cancellablePromise(executor) {
         let cancel;
         const wrapped = new Promise((resolve, reject) => {
             cancel = () => reject(new Error("Cancelled"));
             executor(resolve, reject);
         });
         wrapped.cancel = cancel;
         return wrapped;
     }
    
     const promise = cancellablePromise((resolve, reject) => {
         setTimeout(() => resolve("Done!"), 3000);
     });
    
     promise.then(console.log).catch(console.error);
    
     // Cancel before it completes
     setTimeout(() => promise.cancel(), 1000);
    

    iii) Reactive Libraries / Promises with Cancel Support
    If you're using libraries like,

    RxJS (with Observables)

    Axios (with cancel tokens — now deprecated, but had similar logic)

    Bluebird (with cancellable Promises)

  8. How do you handle promises sequentially?
    Handling Promises sequentially means making sure each Promise runs only after the previous one has finished — super useful for things like - Making API calls in order, Performing async tasks one-by-one, Respecting rate limits.
    i) Using async/await (Cleanest Way)

     async function runSequentially() {
         const result1 = await fetch('https://jsonplaceholder.typicode.com/posts/1');
         const data1 = await result1.json();
         console.log("Post 1:", data1);
    
         const result2 = await fetch('https://jsonplaceholder.typicode.com/posts/2');
         const data2 = await result2.json();
         console.log("Post 2:", data2);
    
         const result3 = await fetch('https://jsonplaceholder.typicode.com/posts/3');
         const data3 = await result3.json();
         console.log("Post 3:", data3);
     }
    
     runSequentially();
    

    ii) Sequentially Loop Through an Array

     const urls = [
         'https://jsonplaceholder.typicode.com/posts/1',
         'https://jsonplaceholder.typicode.com/posts/2',
         'https://jsonplaceholder.typicode.com/posts/3'
     ];
    
     async function fetchSequentially(urls) {
         for (let url of urls) {
             const res = await fetch(url);
             const data = await res.json();
             console.log(data);
         }
     }
    
     fetchSequentially(urls);
    

    iii) Using .then() Chaining

     fetch('https://jsonplaceholder.typicode.com/posts/1')
         .then(res => res.json())
         .then(data1 => {
             console.log("Post 1:", data1);
             return fetch('https://jsonplaceholder.typicode.com/posts/2');
         })
         .then(res => res.json())
         .then(data2 => {
             console.log("Post 2:", data2);
             return fetch('https://jsonplaceholder.typicode.com/posts/3');
         })
         .then(res => res.json())
         .then(data3 => {
             console.log("Post 3:", data3);
         })
         .catch(err => console.error("Error:", err));
    
  9. What is async/await, and What are the benefits of using async/await over promises?
    async/await is syntactic sugar over Promises introduced in ES2017 (ES8). It makes asynchronous code look and behave more like synchronous code — making it easier to read, write, and debug.

     async function fetchData() {
         try {
             const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
             const data = await response.json();
             console.log("Data:", data);
         } catch (error) {
             console.error("Error:", error);
         }
     }
    

    async keyword: declares a function that returns a Promise.

    await keyword: pauses execution until the Promise settles (resolves or rejects).
    Benefits of Using async/await Over Promises -
    i) Clean, readable code
    Feels like synchronous code - easier to understand & maintain
    ii) Easier error handling
    Use try/catch instead of multiple .catch() blocks
    iii) Avoids callback hell
    Reduces deep .then().then().then() nesting
    iv) Great for sequencing
    Easy to run async tasks one after another (sequentially)
    v) Debug-friendly
    Debuggers and stack traces are easier to follow
    When Not to Use await -
    i) When you need parallel execution (Promise.all() is better)

    ii) When handling many short tasks where batching is faster

  10. How do you handle errors with async/await?
    Error handling with async/await is one of its biggest strengths — it makes dealing with asynchronous failures clean and readable using try...catch.

    async function fetchData() {
        try {
            const response = await fetch('https://jsonplaceholder.typicode.com/invalid-url');
    
            if (!response.ok) {
                throw new Error(`HTTP error! Status: ${response.status}`);
            }
    
            const data = await response.json();
            console.log("Data:", data);
        } catch (error) {
            console.error("Caught error:", error.message);
        }
    }
    
    fetchData();
    

    try block - Place your await calls here.

    catch block - Catches: Network errors, JSON parsing errors, Manually thrown errors (like throw new Error())

    Always Check response.ok

    Even if fetch() succeeds (as in: doesn't throw), it can still return a 404 or 500. So always check

    if (!response.ok) {
        throw new Error("Request failed with status: " + response.status);
    }
    
  11. What are generators in Javascript, and how do they work?

    Generators are special functions in JavaScript that can pause and resume their execution.

    They are declared using the function* syntax and use the yield keyword to pause and return a value at each step.

    function* myGenerator() {
        yield "First";
        yield "Second";
        yield "Third";
    }
    
    const gen = myGenerator();
    
    console.log(gen.next()); // { value: "First", done: false }
    console.log(gen.next()); // { value: "Second", done: false }
    console.log(gen.next()); // { value: "Third", done: false }
    console.log(gen.next()); // { value: undefined, done: true }
    // yield → Think of it like a return, but with the ability to come back later.
    

    i) When you call a generator function, it doesn’t run immediately.

    ii) It returns a generator object (an iterator).

    iii) Each call to .next() runs the generator until it hits the next yield.

    function* - Declares a generator function
    yield - Pauses the function and returns a value
    .next() - Resumes the function from where it left off
    done - A flag returned by .next() indicating if the generator has finished

    Generators Are Perfect for -
    i) Lazy loading / streaming values - Generate values on demand
    ii) Custom iterators - Build powerful for...of loops
    iii) Async control flows (via yield) - Advanced use with async tools like redux-saga
    iv) Pausing expensive computations - Break down long tasks into smaller pieces

  12. What are mixins in JavaScript?
    Mixins are a pattern used to add reusable behaviors (methods/properties) to classes or objects, without using inheritance.

    Think of them like “plug-in features” you can mix into multiple classes.
    JavaScript doesn't support multiple inheritance, but you often want a class to share behaviors from multiple sources.

    Mixins solve that by letting you compose functionality.

    let canEat = {
        eat() {
            console.log(`${this.name} is eating`);
        }
    };
    
    let canWalk = {
        walk() {
            console.log(`${this.name} is walking`);
        }
    };
    
    class Person {
        constructor(name) {
            this.name = name;
        }
    }
    
    // Mix behaviors into the class prototype
    Object.assign(Person.prototype, canEat, canWalk);
    
    const user = new Person("Alice");
    user.eat();  // Alice is eating
    user.walk(); // Alice is walking
    
  13. What is currying in JavaScript? Provide an example.
    Currying is a technique where a function, instead of taking all arguments at once, takes them one at a time — returning a new function each time until all arguments are received.
    In short: Convert a function f(a, b) into f(a)(b)

    // Normal function
    function add(a, b) {
        return a + b;
    }
    
    // Curried version
    function curriedAdd(a) {
        return function(b) {
            return a + b;
        };
    }
    
    console.log(add(2, 3));         // 5
    console.log(curriedAdd(2)(3));  // 5
    

    Why Use Currying? -

    i) Reusability - Create specialized functions by pre-filling args
    ii) Functional composition - Easier to chain and combine small functions
    iii) Cleaner syntax - Especially in frameworks like React or Redux

  14. Explain the debouncing, and when would you use it?
    Debouncing is a technique used to limit how often a function is executed — especially when it's triggered by high-frequency events like Typing, Scrolling, Resizing, Button clicks.
    It ensures that the function only runs after a specified delay, and only if the event stops happening during that delay.
    Imagine you're typing in a search box — instead of firing a search API request on every keystroke, you debounce it to wait 300ms after you stop typing.

    function debounce(fn, delay) {
        let timeout;
        return function(...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => {
                fn.apply(this, args);
            }, delay);
        };
    }
    
    // Example usage
    function searchHandler(query) {
        console.log("Searching for:", query);
    }
    
    const debouncedSearch = debounce(searchHandler, 500);
    
    // Simulate typing
    debouncedSearch("H");
    debouncedSearch("He");
    debouncedSearch("Hel");
    debouncedSearch("Hell");
    debouncedSearch("Hello"); // Only this will trigger after 500ms
    

    When to Use Debouncing -
    i) Search inputs - Reduce unnecessary API calls while typing
    ii) Window resize events - Avoid performance hits from rapid firing
    iii) Scroll-based animations/lazy load - Smooth rendering, fewer updates
    iv) Form validation while typing - Don’t validate on every keystroke

  15. Explain the throttling, and when would you use it?
    Throttling is a technique that ensures a function is called at most once every X milliseconds, no matter how many times the event is triggered.

    So even if the event fires constantly, the throttled function only runs at fixed intervals.
    Imagine someone trying to call you 100 times in a minute, but your phone only lets one call through every 5 seconds — that’s throttling.

    function throttle(fn, delay) {
        let lastCall = 0;
        return function(...args) {
            const now = new Date().getTime();
            if (now - lastCall >= delay) {
                lastCall = now;
                fn.apply(this, args);
            }
        };
    }
    
    // Usage
    function logScrollPosition() {
        console.log("Scroll Y:", window.scrollY);
    }
    
    window.addEventListener('scroll', throttle(logScrollPosition, 300));
    // In this case, even if the user scrolls rapidly, 
    // the function logs scroll position at most every 300ms.
    

    When to Use Throttling -
    i) Scroll events - Avoid flooding the browser with too many calls
    ii) Window resize - Optimize layout recalculations
    iii) Button mash prevention - Prevent spamming API or animations
    iv) Continuous input tracking - Log or track at steady intervals

  16. What is difference between debouncing and throttling?
    i) Definition
    Debouncing - Delay execution until after a certain time has passed since the last event.
    Throttling - Limits execution to once every specified time interval, no matter how many events happen.
    ii) When function runs
    Debouncing - After no events occur for the delay period.
    Throttling - At regular intervals during continuous event triggering.
    iii) Purpose
    Debouncing - Ensure the function runs only once after rapid events stop.
    Throttling - Ensure the function runs at steady intervals during rapid events.
    iv) Common Use Cases
    Debouncing - Auto-save, search input filtering, window resizing (after stop typing/resizing).
    Throttling - Scrolling events, mouse movements, window resizing (smooth updates while happening).
    v) Example Behavior
    Debouncing - Wait until user stops typing to send an API request.
    Throttling - Track scrolling position every 300ms, even during heavy scroll.
    vi) If you want
    Debouncing - "Do it once after the user stops doing it"
    Throttling - "Do it regularly while the user is doing it"

  17. Explain the concept of Event propagation?
    Event propagation is the way events travel through the DOM tree.
    It happens in three phases,

    i) Capturing Phase (Trickling Down)

    The event starts at the window and travels down to the target element.
    ii) Target Phase

    The event reaches the target element (where the event actually occurred).
    iii) Bubbling Phase (Bubbling Up)

    The event then bubbles up from the target to the root (window).
    Imagine clicking a button inside a div inside the body:

    windowdocumentbodydivbutton (target)
                                    ↑
                              bubbles back up
    
    <div id="outer">
        <button id="inner">Click me</button>
    </div>
    
    document.getElementById("outer").addEventListener("click", () => {
        console.log("Outer clicked");
    }, true); // Capturing phase
    
    document.getElementById("inner").addEventListener("click", () => {
        console.log("Inner clicked");
    });
    

    Controlling Propagation -
    i) event.stopPropagation()
    Stops further propagation (no bubbling/capturing)
    ii) event.stopImmediatePropagation()
    Also blocks other listeners on the same element
    iii) event.preventDefault()
    Prevents default browser behavior (like form submission)

  18. How do you prevent the default action of an event?
    you can prevent the default action of an event (like a link navigating, a form submitting, etc.) by calling event.preventDefault();

    <form id="myForm">
      <input type="text" name="name" />
      <button type="submit">Submit</button>
    </form>
    
    <script>
    document.getElementById("myForm").addEventListener("submit", function(event) {
        event.preventDefault(); // Stops the form from submitting
        console.log("Form submission prevented!");
    });
    </script>
    
  19. What is the difference between event delegation and event bubbling?
    Event bubbling and event delegation are closely related concepts, but they serve different purposes.
    i) Event Bubbling
    A mechanism where an event starts from the target element and bubbles up through its ancestors.

    <div id="parent">
      <button id="child">Click Me</button>
    </div>
    
    document.getElementById("parent").addEventListener("click", () => {
      console.log("Parent clicked");
    });
    
    document.getElementById("child").addEventListener("click", () => {
      console.log("Child clicked");
    });
    

    Bubbling happens automatically unless stopped with event.stopPropagation().

    ii) Event Delegation
    It is a pattern where you attach one event listener to a parent, and use event.target to detect which child element was clicked.
    It is useful for handling events on dynamically added elements or reducing multiple listeners.

    document.getElementById("parent").addEventListener("click", (event) => {
      if (event.target.tagName === "BUTTON") {
        console.log("Button clicked:", event.target.textContent);
      }
    });
    
  20. How does JavaScript handle memory leaks?
    JavaScript has automatic memory management — meaning it uses a Garbage Collector to free up memory that is no longer needed.
    If something is no longer reachable (nothing references it anymore), the garbage collector removes it from memory. But Memory leaks can still happen when you accidentally keep references to things you no longer need!
    Chrome DevTools, Firefox DevTools, etc. can detect and profile memory leaks.
    Tools like Heap Snapshots help you track down objects still stuck in memory.
    How to Minimize/Prevent Memory Leaks -
    i) Use let, const instead of globals - Limits variable scope
    ii) Clean up timers and listeners - clearInterval(), removeEventListener() when no longer needed
    iii) Nullify references manually - element = null; when done
    iv) Use WeakMap/WeakSet for caching - They don't prevent garbage collection

  21. How do you use Web Workers in JavaScript for multi-threading?
    Web Workers allow JavaScript to run in the background on a separate thread.

    This means they can do heavy computations without blocking the main UI thread — keeping your app smooth and responsive!
    JavaScript is single-threaded by default, but Web Workers give you a way to simulate multi-threading.
    How to Use Web Workers -
    i) Create a separate JS file for the worker code.

    ii) Instantiate a Worker in your main script.

    iii) Use postMessage() to send data to the worker.

    iv) The worker responds back with postMessage() too.

    // worker.js
    
    // Listen for messages from main thread
    self.addEventListener('message', function(event) {
      const result = event.data * 2;  // Simple computation
      self.postMessage(result);       // Send result back
    });
    
    // main.js
    
    // Create a new worker
    const worker = new Worker('worker.js');
    
    // Listen for messages from the worker
    worker.addEventListener('message', function(event) {
      console.log("Result from worker:", event.data);
    });
    
    // Send data to the worker
    worker.postMessage(10);
    

    Why Use Web Workers? -
    i) Heavy calculations (e.g., image processing, video encoding).

    ii) Real-time data crunching (e.g., financial charts, game physics).

    iii) Background syncing, offline tasks.

  22. What is memoization in Javascript, and how is it used?
    Memoization is a caching technique, It stores the results of expensive function calls and returns the cached result when the same inputs occur again.

    This improves performance, especially for heavy or repetitive computations.

    // Simple Example Without Memoization
    function slowFunction(num) {
      console.log("Calculating...");
      return num * num;
    }
    
    console.log(slowFunction(5)); // "Calculating..." then 25
    console.log(slowFunction(5)); // "Calculating..." again!
    // The function recomputes even if the input is the same.
    
    // Memoized Version
    function memoize(fn) {
      const cache = {};
      return function(...args) {
        const key = JSON.stringify(args); // create a unique key based on arguments
        if (cache[key]) {
          return cache[key];
        } else {
          const result = fn.apply(this, args);
          cache[key] = result;
          return result;
        }
      };
    }
    
    // Usage
    const optimizedFunction = memoize(slowFunction);
    
    console.log(optimizedFunction(5)); // "Calculating..." then 25
    console.log(optimizedFunction(5)); // Instant 25 from cache (no "Calculating...")
    // Now the result is cached, and the second call is instant!
    
  23. What are pure functions in JavaScript?
    A pure function is a function that:

    Given the same input, always returns the same output.

    Does not cause any side effects (like modifying a global variable, DOM manipulation, writing to a file, etc.).

    function add(a, b) {
      return a + b;
    }
    
    console.log(add(2, 3)); // Always 5
    console.log(add(2, 3)); // Always 5, no side effects
    

    Why Pure Functions are Important -

    Easier to debug and test.

    Predictable behavior (no surprises!).

    Great for functional programming (React, Redux, etc.).

    Makes code more reusable and maintainable.

  24. What are some best practices for securing JavaScript applications?
    i) Avoid Exposing Sensitive Information -

    Never store API keys, passwords, or secrets in your frontend code.

    Use environment variables on the server side and APIs to handle sensitive tasks.
    ii) Use HTTPS -

    Always serve your website over HTTPS.

    Protects against man-in-the-middle attacks and data sniffing.
    iii) Input Validation and Sanitization -

    Validate and sanitize all user input (both on frontend and backend).

    Prevents attacks like Cross-Site Scripting (XSS) and SQL Injection.
    iv) Prevent Cross-Site Scripting (XSS)

    Never directly inject user input into the DOM.

    Escape or sanitize outputs.

    Use security libraries like: DOMPurify (to sanitize HTML safely)
    v) Use Content Security Policy (CSP)

    Set up a CSP header to restrict what resources the browser can load.

    Helps prevent XSS attacks by blocking unauthorized scripts.

    <meta http-equiv="Content-Security-Policy" content="default-src 'self'">
    

    vi) Be Careful with eval() and new Function()

    Avoid using eval() — it can execute arbitrary code, which is very dangerous.

    Same caution applies to setTimeout(string) and setInterval(string).
    vii) Keep Dependencies Updated

    Vulnerabilities often come through outdated libraries.

    Regularly update your npm/yarn packages.

    Use tools like:npm audit, Snyk, Dependabot
    viii) Use Proper Authentication and Authorization

    Use libraries like OAuth, JWT for authentication.

    Implement role-based access control (RBAC) on the server.

    Never trust client-side authentication alone!

    ix) Protect Against Cross-Site Request Forgery (CSRF)

    For APIs, use anti-CSRF tokens.

    Set appropriate CORS (Cross-Origin Resource Sharing) policies.

    x) Limit Error Messages

    Avoid showing stack traces or internal errors to users.

    Log detailed errors server-side, but show generic errors to users.