Asynchronous JavaScript: From Callbacks to Async/Await - A Journey Through Evolution
Tags: javascript, webdev, node, beginners
Published: 2023-05-01 14:29:55

Asynchronous programming in JavaScript can be a bit of a funky headache, especially when dealing with complex code. In the early days of JavaScript, callbacks were the groovy go-to method for handling asynchronous operations.

> If you prefer to learn through videos, check out this resource that offers a clear explanation of these concepts using examples <a href="https://youtu.be/STA-6p0aL1k" target="_blank"> From Callback to async/await: A Journey through Asynchronous Javascript </a>


<img align="left" src="https://media.giphy.com/media/Q8OnVOm51ws5BotlE3/giphy.gif" alt="callback"/>
 
## Callbacks

Callbacks are functions that are passed as arguments to other functions and are invoked when an asynchronous operation completes. For example:

```js
function getData(callback) {
  // simulate an asynchronous operation
  setTimeout(() => {
    callback('Hello World!');
  }, 1000);
}

// use the getData function with a callback
getData((data) => {
  console.log(data); // output: Hello World!
});
```

Dealing with multiple levels of nesting in callbacks can be a real headache, leading to a phenomenon known as "callback hell." It can be a real hassle when writing complex code in JavaScript. To help you understand this concept better, let me show you an example.

```js
function getData(callback) {
  setTimeout(() => {
    callback('Hello');
    setTimeout(() => {
      callback('World');
      setTimeout(() => {
        callback('!');
      }, 1000);
    }, 1000);
  }, 1000);
}

// use the getData function with a callback
getData((data) => {
  console.log(data); // output: Hello
  getData((data) => {
    console.log(data); // output: World
    getData((data) => {
      console.log(data); // output: !
    });
  });
});
```

<img src="https://media.giphy.com/media/aSVMuNPkLN8QAfwIGD/giphy-downsized-large.gif" alt="3rd callback 4th callback 5th callback"/>

## Promises

Thankfully, the introduction of Promises offered a more elegant solution to handling asynchronous code. Promises represent a value that may not be available yet, but will be at some point in the future. With Promises, we can chain asynchronous operations and handle errors more efficiently. Here's an example:

```js
function getData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('Hello World!');
    }, 1000);
  });
}

// use the getData function with Promises
getData()
  .then((data) => {
    console.log(data); // output: Hello World!
  })
  .catch((error) => {
    console.log(`Error fetching data: ${error}`);
  });
```

While Promises are a significant improvement over callbacks, they still resulted in long chains of then and catch statements, which could make the code hard to read and understand. Not so fun, right?

## async/await

And then came the dynamic duo - async and await! These two made a game-changing entry, allowing developers to write asynchronous code that reads like synchronous code. It's like having a superhero sidekick! Instead of chaining Promises, await can be used to pause the function execution until the Promise resolves. This results in cleaner, more readable code that's easier to maintain and debug. How cool is that?

```js
async function getData() {
  try {
    const data = await fetch('/data');
    return data;
  } catch (error) {
    console.log(`Error fetching data: ${error}`);
  }
}

// use the getData function with async/await
(async () => {
  const data = await getData();
  console.log(data); // output: fetched data
})();
```

As you can see, async/await significantly simplifies asynchronous programming in JavaScript. No more callback hell, no more long chains of then and catch statements. Just clean, concise code that's easy to read and understand.