Synchronous vs Asynchronous JavaScript

JavaScript is single-threaded, meaning it executes one operation at a time in a sequential manner. However, JavaScript provides mechanisms to handle tasks asynchronously, allowing it to perform operations without blocking the main execution thread.

1. Synchronous JavaScript

In synchronous execution, tasks run one after another, meaning JavaScript waits for one operation to complete before moving to the next. This can cause delays if a task takes a long time (e.g., reading a large file or making a network request).

Example:

console.log("Start");
function syncFunction() {
    console.log("Processing...");
}
syncFunction();
console.log("End");

Here, JavaScript executes statements one by one in order, blocking execution until each task is completed.

2. Asynchronous JavaScript

Asynchronous JavaScript allows tasks to run in the background without blocking the execution of other tasks. This is useful for operations like fetching data from an API, reading files, or handling timers.

  • Does NOT wait for one task to complete before moving to the next.
  • Allows JavaScript to handle time-consuming operations (API calls, file reads, etc.) efficiently.
  • Uses the Event Loop to manage async tasks.

How Asynchronous JavaScript Works

JavaScript uses the Event Loop and the Call Stack to handle asynchronous code efficiently. It uses:

  • Callbacks(setTimeout,setInterval)
  • Fetch API
  • Axios
  • Promises
  • Async/Await

Callbacks

A callback function is passed as an argument to be executed later.given example use the both setTimeout and setInterval and clearInterval to stop the setInterval

console.log("Start of program");
// setTimeout: Delays execution by 3 seconds
setTimeout(() => {
    console.log("setTimeout: This runs after 3 seconds");
}, 3000);
// setInterval: Runs every 2 seconds
let counter = 0;
let intervalID = setInterval(() => {
    counter++;
    console.log(`setInterval: Counter is ${counter}`);
    // Stop interval after 5 executions
    if (counter === 5) {
        clearInterval(intervalID);
        console.log("clearInterval: Stopped interval after 5 executions");
    }
}, 2000);
console.log("End of program (before timeouts and intervals execute)");

Fetch API

The fetch() API is asynchronous because it makes network requests without blocking the execution of other code. It returns a Promise, which resolves when the request completes.

fetch("https://jsonplaceholder.typicode.com/todos/1") // Fetch data from API
  .then((response) => response.json()) // Convert response to JSON
  .then((data) => console.log(data)) // Log the data
  .catch((error) => console.error("Error fetching data:", error));

πŸ”Ή How it works? 1️⃣ fetch() makes an HTTP request (returns a Promise).
2️⃣ .json() parses the response into JavaScript object (also async, returns a Promise).
3️⃣ .then() handles the resolved data.
4️⃣ .catch() handles errors (e.g., network failure).

Axios

Axios is asynchronous because it makes network requests without blocking execution. It is built on top of JavaScript Promises, similar to fetch(), but with more features.

Why use Axios?

  • No need to manually parse JSON (Axios does it automatically).
  • Better error handling (Fetch only rejects on network errors, while Axios rejects on HTTP errors too).
  • More readable with async/await.
axios.get("https://jsonplaceholder.typicode.com/todos/1")
    .then(response => console.log(response.data)) // Axios auto-parses JSON
    .catch(error => console.error("Error fetching data:", error));

πŸ”₯ Comparison: Axios vs. Fetch

FeatureAxiosFetch
JSON Parsingβœ… Auto❌ Manual (.json())
Error Handlingβœ… Rejects on HTTP errors❌ Only rejects on network errors
Request Headersβœ… Easy❌ Manual setup required
async/await Supportβœ… Yesβœ… Yes
Default Timeoutβœ… Yes (timeout option)❌ No

πŸ‘‰ Best Practice: Use Axios when working with APIs that require headers, authentication, or advanced error handling.

Promise in JavaScript

A Promise in JavaScript is an object that represents the eventual completion (or failure) of an asynchronous operation.

πŸ‘‰ It has three states:

  1. Pending β†’ Initial state, neither resolved nor rejected.
  2. Fulfilled β†’ Operation completed successfully.
  3. Rejected β†’ Operation failed.
let promise = new Promise(function (resolve, reject) {
  fetch(`https://randomuser.me/api/`)
    .then((raw) => raw.json())
    .then((result) => {
      if (result.results[0].gender === "male") {
        resolve();
      } else {
        reject();
      }
    });
});
promise
  .then(function () {
    console.log("Male Received");
  })
  .catch(function () {
    console.log("Female Received");
  });

Callbacks in JavaScript

A callback is a function passed as an argument to another function and executed later.
πŸ‘‰ Why use callbacks?

  • Handle asynchronous operations (like fetching data, timers, or reading files).
  • Execute code after another function completes.
function getData(url, callback) {
  fetch(`https://randomuser.me/api/`)
    .then((raw) => raw.json())
    .then((result) => {
      callback(result);
    });
}

getData("https://randomuser.me/api/", function (result) {
  console.log(
    result.results[0].gender,
    result.results[0].name.first,
    result.results[0].email,
  );
});

async/await

async/await is a modern way to handle asynchronous code in JavaScript.
It allows you to write asynchronous code in a way that looks synchronous, making it easier to read and debug.

πŸ‘‰ Key Features:
βœ”οΈ **async: **The async keyword is used before a function definition to make it return a Promise. βœ”οΈ await: The await keyword is used inside an async function to pause execution until a Promise resolves. βœ”οΈ Avoids callback hell and complex .then() chains.
βœ”οΈ Handles errors with try/catch.

async function func() {
  let n = await fetch(`https://randomuser.me/api/`);
  n = await n.json();
  console.log(n.results[0].gender);
}
func();

When to Use async/await?

βœ”οΈ Fetching data from an API
βœ”οΈ Performing database queries
βœ”οΈ Reading/Writing files (in Node.js)
βœ”οΈ Processing multiple async requests together
βœ”οΈ Avoiding callback hell

Event Loop

To understand the Event Loop, you need to know how JavaScript executes code. It has three main components:

Call Stack

  • The Call Stack follows the LIFO (Last In, First Out) principle.
  • It executes synchronous JavaScript code line by line.
function greet() {
    console.log("Hello!");
}
greet();

πŸ“Œ Execution:

  1. greet() is pushed onto the Call Stack.
  2. console.log("Hello!") runs and is popped from the stack.

Web APIs (Async Tasks)

  • JavaScript delegates async tasks (e.g., setTimeout, fetch, event listeners) to Web APIs.
  • These APIs handle the task in the background and return the result later.

Example:

console.log("Start");
setTimeout(() => {
    console.log("Inside setTimeout");
}, 1000);

console.log("End");

πŸ“Œ Execution Flow:

  1. console.log("Start") β†’ Executes immediately.
  2. setTimeout() β†’ Sent to Web API, NOT executed immediately.
  3. console.log("End") β†’ Executes immediately.
  4. After 1 second, the callback (console.log("Inside setTimeout")) moves to Callback Queue.

Callback Queue & Microtask Queue

  • Callback Queue: Stores callbacks from setTimeout, setInterval, event listeners.
  • Microtask Queue: Stores callbacks from Promises (.then(), catch(), finally()) and MutationObserver.
  • Microtasks always run before Callbacks (higher priority).

Example:

console.log("Start");
setTimeout(() => console.log("Timeout Callback"), 0);
Promise.resolve().then(() => console.log("Promise Resolved"));
console.log("End");

πŸ“Œ Execution Flow:

  1. console.log("Start") β†’ Runs immediately.
  2. setTimeout() β†’ Sent to Web API (callback moved to Callback Queue).
  3. Promise.resolve().then(...) β†’ Sent to Microtask Queue.
  4. console.log("End") β†’ Runs immediately.
  5. Microtask Queue executes first β†’ console.log("Promise Resolved").
  6. Callback Queue executes β†’ console.log("Timeout Callback").

How the Event Loop Works

1️⃣ JavaScript executes synchronous code in the Call Stack. 2️⃣ If an async task is encountered, it moves to the Web API. 3️⃣ Once completed, it moves the callback to the Callback Queue or Microtask Queue. 4️⃣ The Event Loop checks if the Call Stack is empty. 5️⃣ If empty, it first executes Microtasks, then moves Callbacks from the Callback Queue. 6️⃣ Repeats the process infinitely.

πŸš€ Key Takeaways

βœ” JavaScript is single-threaded but handles async tasks with the Event Loop.
βœ” Microtask Queue (Promises) runs before the Callback Queue (setTimeout).
βœ” The Event Loop ensures JavaScript never blocks execution.
βœ” Long-running tasks should be asynchronous to avoid UI freezing.

Difference Between Callbacks, Promises, and Async/Await

FeatureCallbacksPromisesAsync/Await
DefinitionA function passed as an argument to another function and executed later.An object representing the eventual completion (or failure) of an asynchronous operation.A modern way to handle asynchronous code using async and await keywords.
SyntaxNested functions, leading to callback hell.Uses .then(), .catch(), and .finally().Uses async to declare functions and await to handle promises.
ReadabilityHard to read and maintain due to callback nesting.More readable but still involves chaining.Clean and synchronous-like syntax.
Error HandlingError-prone, requires manual handling (if-else or separate error callback).Uses .catch() to handle errors.Uses try...catch for better error handling.
ChainingDifficult, leads to callback hell (Pyramid of Doom).Easy with .then() chaining.Simplifies chaining with await.
Execution FlowAsynchronous but difficult to manage for multiple tasks.More manageable, but chaining can still be tricky.Most intuitive, executes sequentially like synchronous code.
Best ForSimple async operations, event listeners.Handling multiple asynchronous operations in a cleaner way.Writing clean, readable, and maintainable async code.

Generators in JavaScript

Generators are special functions in JavaScript that allow us to pause and resume execution. Unlike normal functions, they do not execute all at once; instead, they produce values one at a time as requested.

Key Features of Generators

βœ” Uses the function* syntax (notice the * after function).
βœ” Uses the yield keyword to pause execution and return values.
βœ” The function doesn’t run immediately; instead, it returns an iterator object.
βœ” The next() method resumes execution from the last yield.

function* myGenerator() {
    console.log("Start");
    yield 1; // Pause and return 1
    console.log("Resume");
    yield 2; // Pause and return 2
    console.log("End");
}

const gen = myGenerator(); // Creates the generator object

console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next().value); // 2
console.log(gen.next()); // { value: undefined, done: true }

πŸ“Œ How it Works?

  1. Calling myGenerator() doesn’t execute it immediately. It returns a generator object (gen).
  2. gen.next() starts execution until the first yield, returning { value: 1, done: false }.
  3. Calling gen.next() again resumes execution after the first yield, printing "Resume" and yielding { value: 2, done: false }.
  4. When there are no more yield statements, done: true is returned.

Generator with for...of Loop

Instead of calling next() manually, we can use a for...of loop.

function* fruits() {
    yield "🍎 Apple";
    yield "🍌 Banana";
    yield "πŸ‡ Grapes";
}

for (let fruit of fruits()) {
    console.log(fruit);
}

Generators vs Async/Await

FeatureGeneratorsAsync/Await
ExecutionCan be paused/resumed using yieldPauses execution until a Promise resolves
ReturnsIterator objectPromise
Use CaseCustom iteration, state managementHandling async operations

When to Use Generators?

βœ” Custom Iterators – Iterating over data in a custom way.
βœ” Lazy Execution – Generate values on demand instead of all at once.
βœ” Infinite Sequences – Generate values infinitely without memory issues.
βœ” Asynchronous Programming (with co-routines) – Generators can be combined with Promises.

Web Worker

Web Workers allow JavaScript to run background tasks in a separate thread without blocking the main UI thread. This helps in handling heavy computations and keeping the web page responsive.

Why Use Web Workers?

βœ” JavaScript is single-threaded (blocking UI when executing long tasks).
βœ” Web Workers allow running tasks in parallel on a different thread.
βœ” Helps in CPU-intensive tasks like image processing, large calculations, etc.
βœ” Keeps the UI smooth and prevents the page from freezing.

πŸš€ Step 1: Create the Worker (heavyWorker.js)

onmessage = function (data) {
  let ans = data.data.reduce((acc, item) => item + acc, 0);
  postMessage(ans);
};

πŸš€ Step 2: Use the Worker in Your Main Script (main.js)

let nums = Array.from({ length: 10000 }, (_, b) => b + 1);

let worker = new Worker("worker.js");

worker.postMessage(nums);

worker.onmessage = function (data) {
  console.log(data.data);
};

Limitations of Web Workers

❌ No DOM Access – Cannot manipulate HTML directly.
❌ Cannot use window, document, or alert() – Only limited APIs like fetch(), WebSockets, and IndexedDB.
❌ Same-Origin Policy – Workers must be loaded from the same domain.
❌ Extra Resource Consumption – Creating multiple workers consumes more memory.