obah sylva

Callbacks, Promises, and Async/Await: JavaScript Asynchronous Code Explained

Share:fXinW
Share this article

JavaScript is single-threaded, meaning it can only execute one instruction at a time — yet every modern web app fetches data, reads files, and waits on timers without freezing the page. The reason is asynchronous programming, and over the years JavaScript has offered three tools to handle it: callbacks, promises, and async/await. Understanding how these three connect, rather than treating them as competing choices, is one of the most valuable skills a JavaScript developer can build.

Callbacks: Where It All Started

A callback is simply a function passed as an argument to another function, executed once that function’s asynchronous work completes. Callbacks are straightforward for a single async operation — making one HTTP request, for instance — but nesting multiple dependent callbacks quickly produces what the community calls “callback hell”: deeply indented, hard-to-read code where error handling has to be manually repeated at every level using the error-first convention. You’ll still encounter callbacks constantly in legacy codebases and certain Node.js APIs, so understanding them isn’t optional even in 2026.

getUser(userId, function(err, user) {
  if (err) return handleError(err);
  getOrders(user.id, function(err, orders) {
    if (err) return handleError(err);
    getOrderDetails(orders[0].id, function(err, details) {
      if (err) return handleError(err);
      console.log(details);
    });
  });
});

Promises: Solving Callback Hell

A promise is an object representing the eventual completion or failure of an asynchronous operation, with two possible resolutions: resolve (success) or reject (failure). Promises let you chain operations with .then() instead of nesting them, and errors propagate automatically down the chain until they hit a single .catch() block — centralizing error handling in a way callbacks never could. The trade-off: long promise chains can still become difficult to read, even though they’re a clear improvement over deeply nested callbacks.

getUser(userId)
  .then(user => getOrders(user.id))
  .then(orders => getOrderDetails(orders[0].id))
  .then(details => console.log(details))
  .catch(err => handleError(err));

JavaScript code showing async await syntax used for asynchronous programming

Async/Await: Asynchronous Code That Reads Like Synchronous Code

Async/await is built directly on top of promises, adding syntax that lets asynchronous code look and behave like synchronous code. The async keyword declares a function that implicitly returns a promise, and await pauses execution inside that function until the promise resolves — without blocking the rest of the application. Error handling uses familiar try/catch blocks, which most developers find far more intuitive than chained .catch() calls, especially once multiple async operations depend on each other.

async function getOrderDetails(userId) {
  try {
    const user = await getUser(userId);
    const orders = await getOrders(user.id);
    const details = await getOrderDetails(orders[0].id);
    console.log(details);
  } catch (err) {
    handleError(err);
  }
}

Choosing the Right Approach

Async/await is the preferred style for most modern JavaScript in 2026, particularly for sequences of dependent operations, because it’s the most readable and has the cleanest error handling of the three. Promises remain the right tool when you need to run multiple independent operations in parallel — Promise.all() is still the clearest way to express “wait for all of these, in any order.” Callbacks persist mainly in legacy code and specific Node.js APIs that predate promises; you’ll rarely choose them for new code, but you’ll still need to read them.

Regardless of which pattern a codebase uses, the golden rule stays the same: never leave a promise unhandled, and never assume an asynchronous operation will always succeed. Async/await doesn’t remove that responsibility — it just makes the try/catch around it easier to write correctly.

Quick Reference: Async Patterns at a Glance

  • Callbacks: simplest for one-off async tasks, but nested callbacks create hard-to-maintain “callback hell”
  • Promises: chainable with .then()/.catch(), and essential for running parallel operations with Promise.all()
  • Async/await: built on promises, reads like synchronous code, uses try/catch for error handling — the default choice for new code in 2026
  • All three ultimately run on the same JavaScript event loop, so understanding one deepens your understanding of the others
  • Mixing patterns (e.g. calling a promise-returning function inside a callback) is common in real codebases — know how to read all three, even if you write mostly async/await

Frequently Asked Questions

Is async/await faster than promises or callbacks?
No — async/await is syntactic sugar built directly on promises, so performance is effectively identical. Any difference is negligible compared to the readability and maintainability gains.
Can I mix async/await with .then() promise chains?
Yes, and it’s common in real-world code, especially when integrating with libraries that return promises directly. Just be consistent within a single function to keep the code readable.
Do I still need to learn callbacks if async/await is the modern standard?
Yes. Many Node.js core APIs and older libraries still use callback-based patterns, and you’ll encounter them regularly in production codebases regardless of which style you prefer to write.

Want more developer guides like this? Explore more Web Development articles.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top