Callback Functions, Promises, Async/Await and Fetching API's

Syed Aquib AliSyed Aquib Ali
7 min read

Callback Functions

A callback function is a function that is passed as an argument to another function and is executed after some operation has been completed. Callbacks are commonly used for asynchronous operations in JavaScript, such as fetching data from an API, reading files, or handling events.

function greet(name, callback) {
  console.log(`Hello, ${name}!`);
  callback();
}

function sayGoodbye() {
  console.log('Goodbye!');
}

greet("Aquib", sayGoodbye);
// Output:
// Hello, Aquib!
// Goodbye!

In this example, sayGoodbye is passed as a callback to the greet function. After greet logs the greeting, it calls sayGoodbye.

Asynchronous Callbacks

Callbacks are especially useful in asynchronous operations, such as handling API requests or reading files, where you need to perform actions after an operation completes without blocking the execution of the code.

function delayedGreeting(name, callback) {
  setTimeout(() => {
    console.log(`Hello, ${name}!`);
    callback();
  }, 2000); // 2 seconds delay
}

function notify() {
  console.log('Greeting completed!');
}

delayedGreeting("Aquib", notify);
// Output (after 2 seconds delay):
// Hello, Aquib!
// Greeting completed!

In this example, the greeting is delayed by 2 seconds using setTimeout, and after the greeting, the notify function is called.

Using Anonymous Functions as Callbacks

Anonymous functions are often used as callbacks, especially for simple operations.

function processArray(arr, callback) {
  for (let i = 0; i < arr.length; i++) {
    arr[i] = callback(arr[i]);
  }
  return arr;
}

const numbers = [1, 2, 3, 4];
const doubled = processArray(numbers, function(num) {
  return num * 2;
});

console.log(doubled); // Output: [2, 4, 6, 8]

In this example, an anonymous function is passed as a callback to processArray to double each number in the array.

Callback Hell

Callback hell, also known as the "Pyramid of Doom," refers to a situation where callbacks are nested within other callbacks multiple levels deep, making the code hard to read and maintain.

doSomething(function(result1) {
  doSomethingElse(result1, function(result2) {
    doAnotherThing(result2, function(result3) {
      doFinalThing(result3, function(result4) {
        console.log("Done!");
      });
    });
  });
});

In ES6 JavaScript introduced Promise(), this is a replacement of callbacks because as you saw in the last example "Callback Hell", now we can handle the callbacks with even better and easier way.

Promises

Promises are a modern way to handle asynchronous operations in JavaScript. They provide a cleaner, more readable, and more manageable alternative to traditional callback-based approaches. A promise represents a value that may be available now, or in the future, or never.

Promise Structure

  1. Pending: The initial state, neither fulfilled nor rejected.

  2. Fulfilled: The operation completed successfully.

  3. Rejected: The operation failed.

A promise is created using the Promise constructor, which takes a function (executor) with two parameters: resolve and reject.

const myPromise = new Promise((resolve, reject) => {
  const success = true;

  if (success) {
    resolve("Operation was successful!");
  } else {
    reject("Operation failed!");
  }
});

Using Promises

Promises are consumed using the then, catch, and finally methods.

then Method

The then method is used to handle a fulfilled promise.

myPromise.then((message) => {
  console.log(message); // Output: Operation was successful!
});

catch Method

The catch method is used to handle a rejected promise.

const myPromise = new Promise((resolve, reject) => {
  const success = false;

  if (success) {
    resolve("Operation was successful!");
  } else {
    reject("Operation failed!");
  }
});

myPromise.catch((error) => {
  console.error(error); // Output: Operation failed!
});

finally Method

The finally method is executed regardless of the promise's outcome, whether it is fulfilled or rejected.

myPromise.finally(() => {
  console.log("Promise has been settled."); // Output: Promise has been settled.
});

Chaining Promises

Promises can be chained to handle a sequence of asynchronous operations.

const fetchData = new Promise((resolve, reject) => {
  setTimeout(() => resolve("Data fetched"), 1000);
});

fetchData
  .then((data) => {
    console.log(data); // Output: Data fetched
    return "Processing data";
  })
  .then((processMessage) => {
    console.log(processMessage); // Output: Processing data
    return "Data processed";
  })
  .then((finalMessage) => {
    console.log(finalMessage); // Output: Data processed
  })
  .catch((error) => {
    console.error(error);
  })
  .finally(() => {
    console.log("All operations completed"); // Output: All operations completed
  });

Let's understand this with a small demonstration, we will order a pizza:

function orderPizza () {
    return new Promise(function(res, rej) {
        setTimeout(() => {
            const pizza = "pizza";
            res(pizza);
            rej("An error occured while ordering pizza.")
        }, 2000);
    });
}

const pizzaPromise = orderPizza();

pizzaPromise.then((pizza) =>{
    console.log(`my ${pizza} has been ordered.`)
}).catch(error => {
    console.log(error)
}) // Output: My pizza has been ordered.

what would happen if we were not able to place the order? let's see:

function orderPizza () {
    return new Promise(function(res, rej) {
        setTimeout(() => {
            const pizza = "pizza";
            //res(pizza);
            rej("An error occured while ordering pizza.")
        }, 2000);
    });
}

const pizzaPromise = orderPizza();

pizzaPromise.then((pizza) =>{
    console.log(`my ${pizza} has been ordered.`)
}).catch(error => {
    console.log(error)
}) // Output: An error occured while ordering pizza.
  • Here there was no response res so it went for reject rej and threw an error inside .catch.

Async/Await

Async/await is a modern syntax in JavaScript that allows you to write asynchronous code in a more synchronous and readable manner. It is built on top of Promises and provides a way to handle asynchronous operations more gracefully without deeply nested callbacks or complex promise chains.

The async Keyword

The async keyword is used to define an asynchronous function. An async function always returns a Promise. If the function returns a value, the Promise will be resolved with that value. If the function throws an error, the Promise will be rejected with that error.

Syntax

async function functionName() {
  // function body
}
async function greet() {
    return "Hello, World!";
}

greet().then((message) => console.log(message)); // Output: Hello, World!

The await Keyword

The await keyword is used to pause the execution of an async function until a Promise is resolved or rejected. It can only be used inside async functions. await makes the code wait for the Promise to resolve and returns the resolved value. If the Promise is rejected, await throws the rejected value.

Syntax

let result = await promise;
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function sayHello() {
  console.log('Start');
  await delay(2000); // Waits for 2 seconds
  console.log('Hello!');
  console.log('End');
}

sayHello();
// Output:
// Start
// (2 seconds delay)
// Hello!
// End

Combining async and await

Let's combine async and await to perform asynchronous operations in a more readable way. We will again be creating an order pizza function:

function getCheese() {
    return new Promise((res, rej) => {
        setTimeout(() => {
            res("๐Ÿง€");
        }, 1000);
    });
}

function getDough(cheese) {
    return new Promise((res, rej) => {
        setTimeout(() => {
            res(`${cheese} ๐Ÿซ“`);
        }, 2000);
    });
}

function getPizza(dough) {
    return new Promise((res, rej) => {
        setTimeout(() => {
            res(`${dough} ๐Ÿ•`);
        }, 2000);
    });
}

async function orderPizza() {
    const cheese = await getCheese();
    const dough = await getDough(cheese);
    const pizza = await getPizza(dough);

    return `Here is your ${pizza}`;
}

orderPizza().then((pizza) => {
    console.log(pizza);
}) // Output: (after 5 seconds) Here is your ๐Ÿง€ ๐Ÿซ“ ๐Ÿ•

Async/await makes asynchronous code easier to read and maintain by allowing you to write it in a synchronous style.

fetch API

The Fetch API is a modern, promise-based method for making HTTP requests in JavaScript. It provides a more powerful and flexible feature set than the older XMLHttpRequest and is much easier to use.

Usage

The fetch function is used to make a request to a specified URL. It returns a promise that resolves to the response of the request.

Syntax

fetch(url, options)
  .then(response => {
    // handle the response
  })
  .catch(error => {
    // handle errors
  });

We will only understand the GET request for now, Here's a real life example:

async function fetchData() {
    try {                        // Random API from the internet.
        const response = await fetch("https://dummyjson.com/products/2");
        console.log(response);
                                // Converting to JSON format.
        const data = await response.json();
        console.log(data);

        //loadData function will be created on next JavaScript example.
        loadData(data);

    } catch (err) {
        console.log(err)
    }
}

fetchData(); // Output: A big object filled with many data's.

Now let's take an example that we want to use this data dynamically inside of our webpage:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- Very basic code -->
    <h2 id="title">dummy text</h2>
    <img id="img" src="" alt="">

    <script src="fetch.js"></script>
</body>
</html>

In the last JavaScript example we called our function by fetchData(), after that line we will do a simple DOM manipulation:

function loadData(data) {
    const title = document.getElementById("title");
    const img = document.getElementById("img")
    title.innerHTML = data.brand
    img.src = data.thumbnail
}
  • Now the data will be dynamically added to the webpage without writing anything, you can add more fields by observing whats inside of the data object printed on your console.

On the creation of a dynamic website it is essential to understand how to fetch the data from the given API, not just GET request but POST request as well.

0
Subscribe to my newsletter

Read articles from Syed Aquib Ali directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Syed Aquib Ali
Syed Aquib Ali

I am a MERN stack developer who has learnt everything yet trying to polish his skills ๐Ÿฅ‚, I love backend more than frontend.