Understanding async code in Javascript in 2024

Callbacks: The Early Days

The foundation of asynchronous JavaScript was built upon callbacks. A callback is a function passed into another function, to be invoked when a long-running operation completes. Let's illustrate with a simple example:

function fetchData(callback) {
  // Simulate a network request
  setTimeout(() => {
    const data = { message: 'Greetings from the server!' };
    callback(data);
  }, 2000); // Simulate a 2-second delay
}

fetchData((data) => {
  console.log(data.message); 
});

Promises: A Cleaner Approach

Promises brought much-needed structure to the asynchronous world. A promise represents the eventual result of an asynchronous operation. It exists in one of three states:

  • Pending: The operation is ongoing.

  • Fulfilled: The operation completed successfully.

  • Rejected: The operation failed.

Let's refactor our example using promises:

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = { message: 'Greetings from the server!' };
      resolve(data); 
     }, 2000);
  });
}

fetchData()
  .then((data) => console.log(data.message))
  .catch((error) => console.error(error));

Async/Await: Syntactic Sugar

The async and await keywords brought a revolution, making promise-based asynchronous code appear almost synchronous. It's a layer on top of promises, providing a cleaner way to write code that looks like "wait here until this is done."

Let's streamline our example further:

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error(error);
  }
}

fetchData().then((data) => console.log(data.message));