Skip to main content

Command Palette

Search for a command to run...

Async/Await in JavaScript: Writing Cleaner Asynchronous Code

Published
4 min read
Async/Await in JavaScript: Writing Cleaner Asynchronous Code

Async/Await in JavaScript is a modern way to handle asynchronous operations in a simple and readable manner. Before async/await, developers relied on callbacks and promises, which often made code harder to understand.

Async/await was introduced in ES2017 to make asynchronous code look and behave more like synchronous code. This makes your programs easier to read, write, and debug.

Code Example before async/await :

getUserData(userId)
  .then(user => {
    return getOrders(user.id);
  })
  .then(orders => {
    return getOrderDetails(orders[0].id);
  })
  .then(details => {
    return sendConfirmation(details.email);
  })
  .then(result => {
    console.log("Done!", result);
  })
  .catch(err => {
    console.error("Something went wrong:", err);
  });

It works. It's better than callback hell. But it still requires a kind of mental gymnastics while tracking what each .then() receives, what it returns, and where errors bubble up from.

Now look at the same logic written with async/await:

async function processOrder(userId) {
  try {
    let user = await getUserData(userId);
    let orders = await getOrders(user.id);
    let details = await getOrderDetails(orders[0].id);
    let result = await sendConfirmation(details.email);
    console.log("Done!", result);
  } catch (err) {
    console.error("Something went wrong:", err);
  }
}

Same operations. Same asynchronous behavior underneath. But now it reads top to bottom, like a recipe. Each step is clear. Error handling is in one place. You can follow the logic without mentally unwrapping .then() chains.

1. Why Async/Await Was Introduced

Before async/await:

Callbacks caused messy nested code (callback hell).

Promises were a massive improvement over callbacks. They eliminated callback hell and gave us a cleaner way to sequence async operations. But as applications grew more complex, Promise chains developed their own friction.

Async/await simplifies both.

2. Understanding Async Functions

What is an Async Function?

An async function is a function declared using the async keyword.

  • The function always returns a Promise : no matter what you write inside it

  • The await keyword becomes available inside it

How Async Functions Work ?

When you use async, JavaScript automatically wraps the return value in a promise.

async function greet() {
  return "Hello";
}

greet().then(console.log);

You wrote return "Hello!" , but because the function is async, JavaScript automatically wraps that value in Promise.resolve("Hello!"). The function looks like it returns a string. It actually returns a resolved Promise.

3. The Await Keyword Concept

It's the keyword that actually makes asynchronous code look synchronous.

Put await in front of a Promise, and JavaScript will:

  1. Pause execution of the async function at that line (Not whole program, rest of the js continues running normally)

  2. Wait for the Promise to resolve

  3. Unwrap the resolved value and return it

  4. Resume the function from the next line

Example :

async function getData() {
  let response = await fetch("https://api.example.com/data");
  let data = await response.json();
  console.log(data);
}

4. Async/Await as Syntactic Sugar

What is Syntactic Sugar?

Syntactic sugar means writing code in a simpler, cleaner way without changing how it works internally.

How It Simplifies Promises :

Async/await is built on top of promises but removes the need for .then() chains.

Comparison With Promises

Async/await and Promises are not competing approaches, they're the same underlying mechanism with different surfaces.

Examples :

Promises:

fetch(url)
  .then(res => res.json())
  .then(data => console.log(data));

Async/Await:

let res = await fetch(url);
let data = await res.json();
console.log(data);

5. Error Handling with Async Code

With Promises, you handle errors with .catch() at the end of the chain, or in each .then(). It works, but it separates your error handling from your logic:

fetchUser(id)
  .then(user => fetchOrders(user.id))
  .then(orders => processOrders(orders))
  .catch(err => console.error(err));  // handles errors from anywhere above

Async/await allows simple error handling using try...catch.

async function processUserOrders(id) {
  try {
    let user = await fetchUser(id);
    let orders = await fetchOrders(user.id);
    let result = await processOrders(orders);
    console.log("Success:", result);
  } catch (err) {
    console.error("Failed:", err.message);
  }
}

The try block contains your happy path. The catch block handles any failure, from any await in the try block. One error handler covers the whole flow.

Conclusion

Async/await didn't change how JavaScript handles asynchronous operations. Promises still power everything underneath. The event loop still coordinates the task queue. None of that changed.

What changed is how you express that logic, and that change in expression has a surprisingly large impact on how readable, debuggable, and maintainable your code is.

The mental model is simple: mark a function async, and inside it you can await any Promise. The function pauses at each await without blocking the main thread, collects the result, and continues. Errors are handled with try/catch/finally, the same way you handle synchronous errors.