javascript

Asynchronous JavaScript: Promises, Async/Await, and Callbacks

Last updated on

Asynchronous JavaScript: Promises, Async/Await, and Callbacks

Introduction

Asynchronous programming is a crucial aspect of JavaScript that allows developers to execute tasks concurrently, without blocking the main thread. Understanding how to handle asynchronous operations effectively is vital for building modern web applications. In this blog post, we will dive deep into mastering asynchronous JavaScript, exploring three powerful techniques: Promises, Async/Await, and Callbacks. We’ll cover the basics, provide practical code examples, and help you choose the right approach for different scenarios.

1. Callback

Callbacks have been a fundamental part of asynchronous programming in JavaScript for a long time. A callback is a function passed as an argument to another function and gets executed after the completion of an asynchronous operation. While callbacks can be useful, they can lead to callback hell or the pyramid of doom when dealing with multiple nested callbacks.

Example - Callback Hell:

function fetchDataFromAPI(callback) {
  fetch('https://api.example.com/data', (response) => {
    response.json((data) => {
      processData(data, (result) => {
        displayData(result, () => {
          // More callbacks can be nested here
        })
      })
    })
  })
}

Example 2 - Fetching Data with Callbacks:

Suppose we have a weather API, and we want to retrieve the weather forecast for a city. We’ll use a callback function to handle the response.

function fetchWeatherData(city, callback) {
  const apiUrl = `https://api.weather.example.com/forecast/${city}`
  fetch(apiUrl, (response) => {
    if (response.status === 200) {
      response.json((data) => {
        callback(data)
      })
    } else {
      callback(null, 'Error: Unable to fetch weather data.')
    }
  })
}
 
// Usage
fetchWeatherData('New York', (data, error) => {
  if (error) {
    console.error(error)
  } else {
    console.log(data)
  }
})

2. Promises

Promises provide a cleaner and more structured way to handle asynchronous operations. A Promise represents a value that may not be available yet but will resolve in the future, either with a value or an error. Promises have three states: “pending,” “fulfilled,” or “rejected.” They help avoid callback hell by chaining multiple asynchronous operations sequentially.

Example - Using Promises:

In this example we are fetching same weather data for cities using promises

const fetchWeatherData = (city) => {
  const apiUrl = `https://api.weather.example.com/forecast/${city}`
 
  return new Promise((resolve, reject) => {
    fetch(apiUrl)
      .then((response) => {
        if (response.status === 200) {
          resolve(response.json())
        } else {
          reject(new Error('Unable to fetch weather data.'))
        }
      })
      .catch((error) => {
        reject(error)
      })
  })
}
 
// Usage
fetchWeatherData('New York')
  .then((data) => console.log(data))
  .catch((error) => console.error(error))

3. Async/Await

Async/Await is a syntactical improvement over Promises, making asynchronous code look more like synchronous code. It allows developers to write asynchronous code that is easier to read and maintain. Under the hood, Async/Await is built on top of Promises.

Example - Using Async/Await:

In this example, we’ll use Async/Await to fetch weather data for multiple cities.

const fetchWeatherData = async (city) => {
  const apiUrl = `https://api.weather.example.com/forecast/${city}`
  const response = await fetch(apiUrl)
 
  if (response.status === 200) {
    return response.json()
  } else {
    throw new Error('Unable to fetch weather data.')
  }
}
 
// Usage
const fetchWeatherForCities = async (cities) => {
  try {
    for (const city of cities) {
      const data = await fetchWeatherData(city)
      console.log(`Weather in ${city}:`, data)
    }
  } catch (error) {
    console.error(error)
  }
}
 
fetchWeatherForCities(['New York', 'London', 'Tokyo'])

Conclusion

In this blog post, we explored three techniques for mastering asynchronous JavaScript: Callbacks, Promises, and Async/Await, all using ES6 syntax. Callbacks are the traditional approach, while Promises and Async/Await provide more structured and maintainable code, especially for multiple asynchronous tasks.

By leveraging ES6 features, you can build robust and efficient JavaScript applications, making the most of the latest advancements in the language. Choose the approach that best suits your project and team, and apply it to create powerful, responsive, and user-friendly web applications.

Happy coding!