DEV Community

Nikita Maharana
Nikita Maharana

Posted on

Callbacks, Promises & Async – await Explained

Callbacks, Promises & Async – await:

Callbacks -> Callback Hell -> Promises -> Promise Chaining -> Async/Await -> IIFE

Synchronous vs Asynchronous

Synchronous
The code that runs line by line is called a synchronous code. Each instruction waits for the previous one to finish before moving on to the next.But the problem is it can block the UI if a task is slow.

Asynchronous
The code that takes some time to get executed or slow tasks (timers, API calls) are handed off. If there are multiple lines of code before and after the asynchronous code then remaining code keeps running and it does not wait for the asynchronous code to get executed. Result is handled later when ready.

// Async demo with setTimeout
console.log("one");
console.log("two");

setTimeout(() => {
  console.log("hello");  // runs after 4s
}, 4000);

console.log("three");
console.log("four");

// Output: one → two → three → four → hello

Enter fullscreen mode Exit fullscreen mode

Callbacks
A callback is a function passed as an argument to another function and called inside it. It's how JS handles "do this first, then do that" logic.

function sum(a, b) {
  console.log(a + b);
}

function calculator(a, b, sumCallback) {
  sumCallback(a, b);  // sum is called here
}

calculator(1, 2, sum);  // Output: 3

Enter fullscreen mode Exit fullscreen mode

Callback Hell (Pyramid of Doom)
When callbacks are nested inside each other to sequence async tasks, code becomes deeply indented and hard to read and maintain. This is called Callback Hell.
eg:

function getData(dataId, getNextData) {
  setTimeout(() => {
    console.log("data", dataId);
    if (getNextData) getNextData();
  }, 2000);
}

// Callback Hell 
getData(1, () => {
  console.log("getting data2...");
  getData(2, () => {
    console.log("getting data3...");
    getData(3, () => {
      console.log("getting data4...");
      getData(4);
    });
  });
});

Enter fullscreen mode Exit fullscreen mode

The deeper the nesting, the harder to read, debug, and maintain. Promises solve this issue.

Promises
A Promise is a JS object representing the eventual completion or failure of an asynchronous task. It takes a function with two handlers: resolve (success) and reject (failure).
There are 3 states of a promise, they are
i.Pending
Result is undefined. Task still in progress.
ii.Fulfilled
resolve(value) was called. Task succeeded.
iii.Rejected
reject(error) was called. Task failed.

// Creating a Promise
const getPromise = () => {
  return new Promise((resolve, reject) => {
    console.log("I am a promise");
    resolve("success");   // OR
    // reject("error");
  });
};

// Consuming a Promise
let promise = getPromise();

promise.then((res) =>{
console.log("Fulfilled:", res);//runs if promise gets resolved or fullfilled
});

promise.catch((err) => {
  console.log("Rejected:", err);  // runs if promise gets rejected
});

Enter fullscreen mode Exit fullscreen mode

resolve and reject are callbacks provided by JS -- you just call them. If you reject without .catch(), you get an Uncaught (in promise) error in console.

Promise Chaining
Instead of nesting, return a new Promise inside .then() and chain another .then(). This keeps code flat and readable -- this is the fix for callback hell.
eg:

function getData(dataId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("data", dataId);
      resolve("success");
    }, 3000);
  });
}

// Promise Chain (clean & flat!)
getData(1)
  .then((res) => { return getData(2); })
  .then((res) => { return getData(3); })
  .then((res) => { console.log(res); });

Enter fullscreen mode Exit fullscreen mode

Async / Await
When we use async before a function makes it always return a Promise.

await pauses execution inside the async function until a Promise settles -- making async code look and read like synchronous code.

function getData(dataId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("data", dataId);
      resolve("success");
    }, 2000);
  });
}

// Async-Await
async function getAllData() {
  console.log("getting data1...");
  await getData(1);   // waits here
  console.log("getting data2...");
  await getData(2);   // then waits here
  console.log("getting data3...");
  await getData(3);
}
getAllData();

Enter fullscreen mode Exit fullscreen mode

Still in async await we always have to keep the await data inside a function and every time call it to show the execution..to solve this issue we have IIFE...

IIFE — Immediately Invoked Function Expression

An IIFE is a function that runs immediately as soon as it is defined. It only executes once. Useful to run async code at the top level without wrapping it in a named function.

// Regular IIFE
(function () {
  // runs immediately
})();

// Arrow IIFE
(() => {
  // runs immediately
})();

// Async IIFE — most common use case
(async () => {
  console.log("getting data1...");
  await getData(1);
  console.log("getting data2...");
  await getData(2);
})();

Enter fullscreen mode Exit fullscreen mode

*Pattern: *(func)() — wrap function in () to make it an expression, then call it with () immediately.

Summary
Callbacks → Callback Hell problem → Promises fix it → Promise Chaining improves readability → Async/Await makes it look synchronous → IIFE lets you run async code immediately.

Top comments (0)