Advanced Async Concepts

  • This lesson dives into advanced asynchronous patterns and optimization strategies.
  • Introduction to Advanced Asynchronous JavaScript

    Modern JavaScript applications rely heavily on asynchronous behavior to stay fast, responsive, and scalable.

    Asynchronous JavaScript helps:

    • Prevent UI freezing

    • Handle API calls efficiently

    • Manage timers and background tasks

    • Improve overall performance

    In this lesson, we will deeply understand:

    1. Microtasks vs Macrotasks

    2. setTimeout and setInterval

    3. Avoiding Callback Hell

    4. Writing Clean Asynchronous Code

    Microtasks vs Macrotasks

    Why Task Queues Matter

    JavaScript is single-threaded, but it can handle asynchronous operations using:

    • Web APIs

    • Task queues

    • Event Loop

    When asynchronous code finishes, its callback is placed into a queue waiting for execution.

    There are two main types of queues:

    1. Macrotask Queue

    2. Microtask Queue

    What Are Macrotasks ?

    Macrotasks are tasks that are executed after the current call stack is empty and after all microtasks are completed.

    Examples of macrotasks:

    • setTimeout

    • setInterval

    • setImmediate (Node.js)

    • UI rendering events

    What Are Microtasks ?

    Microtasks are tasks that have higher priority than macrotasks.

    They are executed:

    • Immediately after the current synchronous code

    • Before any macrotask

    Examples of microtasks:

    • Promise.then()

    • Promise.catch()

    • Promise.finally()

    • queueMicrotask()

    Execution Priority Order

    1. Call Stack (synchronous code)

    2. Microtask Queue

    3. Macrotask Queue

    4. Rendering (browser)

Microtask vs Macrotask Execution Order

Demonstrates JavaScript event loop order where microtasks run before macrotasks.

console.log("Start");

setTimeout(() => {
  console.log("Macrotask");
}, 0);

Promise.resolve().then(() => {
  console.log("Microtask");
});

console.log("End");
  • Explanation

    • Synchronous code runs first

    • Promise callback (microtask) runs next

    • setTimeout callback (macrotask) runs last

    Why Microtasks Have Higher Priority

    Microtasks ensure:

    • Promise chains complete properly

    • Consistent async behavior

    • Reliable state updates before rendering

    setTimeout & setInterval

    setTimeout()

    What It Does

    Executes a function once after a specified delay.

    Syntax

    setTimeout(function, delayInMilliseconds);

Delayed Execution with setTimeout

Runs a function after a specified delay using setTimeout.

setTimeout(() => {
  console.log("This runs after 2 seconds");
}, 2000);
  • Important Notes About setTimeout

    • Delay is minimum, not guaranteed exact time

    • Callback runs only when call stack is empty

    • It is a macrotask

Clearing a setTimeout Timer

Cancels a scheduled timeout using clearTimeout before it executes.

let timerId = setTimeout(() => {
  console.log("Hello");
}, 3000);

clearTimeout(timerId);
  • setInterval()

    What It Does

    Executes a function repeatedly at a fixed time interval.

    Syntax

    setInterval(function, intervalInMilliseconds);

Repeated Execution with setInterval

Runs a task repeatedly at fixed intervals and stops it using clearInterval.

let count = 1;

let intervalId = setInterval(() => {
  console.log("Count:", count);
  count++;

  if (count > 5) {
    clearInterval(intervalId);
  }
}, 1000);
  • Difference Between setTimeout and setInterval

    Feature

    setTimeout

    setInterval

    Execution

    Once

    Repeated

    Use case

    Delay task

    Periodic task

    Control

    clearTimeout

    clearInterval

    Avoiding Callback Hell

    What Is Callback Hell ?

    Callback Hell occurs when:

    • Callbacks are nested inside callbacks

    • Code becomes deeply indented

    • Logic becomes hard to read and maintain

Callback Hell in JavaScript

Shows deeply nested callbacks that make code hard to read and maintain.

loginUser(user, function () {
  getProfile(function () {
    getOrders(function () {
      getPayments(function () {
        console.log("All data loaded");
      });
    });
  });
});
  • Problems:

    • Poor readability

    • Difficult debugging

    • Error handling becomes complex

    Why Callback Hell Is Dangerous

    • Hard to scale code

    • Difficult error handling

    • Increased bug probability

    • Poor maintainability

    Solution 1: Named Functions

Avoiding Callback Hell with Named Functions

Uses separate named functions to simplify and organize asynchronous flow.

function loadPayments() {
  console.log("Payments loaded");
}

function loadOrders() {
  loadPayments();
}

function loadProfile() {
  loadOrders();
}

loadProfile();
  • Improves readability slightly but still limited.

    Solution 2: Promises

Avoiding Callback Hell with Promises

Uses Promise chaining to simplify asynchronous code and improve readability.

loginUser()
  .then(getProfile)
  .then(getOrders)
  .then(getPayments)
  .then(() => console.log("All data loaded"))
  .catch(error => console.error(error));
  • Much cleaner and readable.

    Topic 4: Writing Clean Async Code

    Best Practice 1: Use async / await

    async/await makes asynchronous code look synchronous.

Avoiding Callback Hell with Async/Await

Uses async/await to write clean and sequential asynchronous code with error handling.

async function loadData() {
  try {
    await loginUser();
    await getProfile();
    await getOrders();
    await getPayments();
    console.log("All data loaded");
  } catch (error) {
    console.error(error);
  }
}

loadData();
  • Best Practice 2: Always Handle Errors

    Never ignore errors in async code.

    Bad:

    fetchData();

    Good:

    try {

      await fetchData();

    } catch (err) {

      console.error(err);

    }

    Best Practice 3: Avoid Unnecessary await

    Bad:

    await console.log("Hello");

    Good:

    console.log("Hello");

    Only await promises.

    Best Practice 4: Run Independent Tasks in Parallel

    let [users, posts] = await Promise.all([

      fetchUsers(),

      fetchPosts()

    ]);

    Improves performance significantly.

    Best Practice 5: Keep Async Functions Small

    • One responsibility per function

    • Easy testing

    • Better reuse

    Common Beginner Mistakes

    • Mixing callbacks with promises

    • Forgetting await

    • Not using try...catch

    • Blocking the main thread

    • Overusing setTimeout

    Real-World Use Cases

    • API calls

    • Authentication

    • Background syncing

    • Timers and polling

    • Animations and transitions