Understanding Asynchronous Programming: Callbacks, Promises, and async/await

Ashiya AmanullaAshiya Amanulla
2 min read

In modern JavaScript development, handling asynchronous operations is a core skill. Whether you're fetching data from an API, reading files, or setting timers, understanding how JavaScript manages non-blocking operations is essential. In this post, we'll dive into three key concepts: callbacks, promises, and async/await.


What Is Synchronous vs Asynchronous Code?

JavaScript is single-threaded, which means it can execute only one task at a time. In synchronous programming, tasks run one after another, and each task must complete before the next one begins and it runs in the specific order. This can become problematic when dealing with time-consuming tasks like network requests.

// Synchronous
console.log("Start");
console.log("Middle");
console.log("End");

// Output:
// Start
// Middle
// End

// Asynchronous
console.log("Start");
setTimeout(() => {
  console.log("Middle");
}, 1000);
console.log("End");

// Output:
// Start
// End
// Middle

Asynchronous code allows the program to continue running while waiting for other operations to complete.


Callbacks – The OG Way

A callback is a function passed as an argument to another function. It's invoked after the parent function completes its operation.

function greet(name, callback) {
  console.log("Hello, " + name);
  callback();
}

function sayBye() {
  console.log("Goodbye!");
}

greet("Alice", sayBye);

Callbacks work fine for simple tasks but can lead to "callback hell" when you have multiple nested asynchronous operations:

doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doAnotherThing(newResult, function(finalResult) {
      console.log(finalResult);
    });
  });
});

Promises – A Cleaner Alternative

Promises provide a more structured way to handle asynchronous operations. A promise represents a value that may be available now, later, or never.

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data received");
    }, 1000);
  });
}

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

A promise has three states:

  • Pending: Initial state

  • Fulfilled: Operation completed successfully

  • Rejected: Operation failed


async/await – Syntactic Sugar

Introduced in ES2017, async and await simplify working with promises and make asynchronous code look synchronous.

async function getData() {
  try {
    const data = await fetchData();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

getData();

This approach enhances code readability and makes error handling more intuitive with try/catch.


When to Use What?

  • Callbacks: Still useful for simple tasks or when working with legacy code.

  • Promises: Ideal for chaining asynchronous operations.

  • async/await: Best for writing readable, modern asynchronous code.


Conclusion

Understanding how JavaScript handles asynchronous operations is crucial for modern development. Happy coding!

0
Subscribe to my newsletter

Read articles from Ashiya Amanulla directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ashiya Amanulla
Ashiya Amanulla