Skip to main content

Command Palette

Search for a command to run...

Async/Await in JavaScript: Writing Cleaner Asynchronous Code

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

Before you deep dive into async await in JS, it is very important you understand what is synchronous and asynchronous behaviour in JavaScript, If you aren't aware of it or need a quick refreshed, I would recommend you this blog.

Why async / await ?

Before the concept of async await, developers used to rely on promises and callbacks for handling asynchronous operations.

Suppose you have an operation where you need to get user, his/her post, the comments made on the posts then you would need the below functions:

function getUser(userId, callback) {
  setTimeout(() => {
    console.log("Fetched user");
    callback({ id: userId, email: "user@example.com" });
  }, 500);
}

function getPosts(userId, callback) {
  setTimeout(() => {
    console.log("Fetched posts");
    callback([{ id: 101 }, { id: 102 }]);
  }, 500);
}

function getComments(postId, callback) {
  setTimeout(() => {
    console.log("Fetched comments");
    callback(["Nice post!", "Great work!"]);
  }, 500);
}

function saveComments(comments, callback) {
  setTimeout(() => {
    console.log("Comments saved");
    callback("success");
  }, 500);
}

function notifyUser(email, callback) {
  setTimeout(() => {
    console.log(`Notification sent to ${email}`);
    callback();
  }, 500);
}

You could do it using callbacks, there are simple and easy to implement, but you never know when you would end up in a callback hell like the one below:

getUser(userId, function (user) {
  getPosts(user.id, function (posts) {
    getComments(posts[0].id, function (comments) {
      console.log("Comments:", comments);

      saveComments(comments, function (result) {
        console.log("Saved:", result);

        notifyUser(user.email, function () {
          console.log("User notified");
        });
      });
    });
  });
});

With the introduction of promises, things became a bit simple, below is how you do the same thing using promises:

getUser(userId)
  .then(user => getPosts(user.id))
  .then(posts => getComments(posts[0].id))
  .then(comments => saveComments(comments))
  .then(() => notifyUser())
  .catch(err => console.error(err));

Even now this chaining can quickly become hard to read and reason about, especially when logic grows.

This was where async/await was introduced to make asynchronous code look and behave more like synchronous code. It improved clarity without changing how it works under the hood.

How async functions work ?

The async keyword is designed in a way that it shall always return a promise.

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

The above code is equivalent to:

function greet(){
  return Promise.resolve("Hello");
}

So anything, that is returned from an async function even if it is a simple value is wrapped inside a Promise.

The await keyword

The await keyword only works inside the an async function.

It pauses the execution of async function until a Promise is resolved.

async function getData() {
  const response = await fetch("api/data");
  const data = await response.json();
  console.log(data);
}

await makes asynchronous code look like synchronous , It improves the readability from the promises .then() chaining, and makes you feel that the code is getting executed line by line.

Error Handling with async code

When you are using promises, then you handle the error caught using the .catch() block

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

In the above code block, you have a single catch block at the very end , because the errors keep bubbling up unless there is catch block found. But you can have multiple catch blocks each handling the error just above it.

However using try...catch block inside an async function feels more natural.

async function fetchData() {
  try {
    const res = await fetch("api/data");
    const data = await res.json();
    console.log(data);
  } catch (err) {
    console.error("Error:", err);
  }
}

This makes error handling cleaner and easier to follow.

Async/Await as syntactic sugar

Async/ await isn't a separate feature, but it is built on the top of promises

async is nothing but simply a promise wrapper and await simply stops the execution of the async function until the promise is resolved.

function delay(ms) {
  return new Promise(resolve =>
    setTimeout(() => {
      console.log("delay");
      resolve();
    }, ms)
  );
}

async function run() {
  console.log("Start");

  await delay(2000);

  console.log("After 2 seconds");
}

run();

Async await helps increase readability as it:

  • Removes heavy .then() chaining

  • Makes code flow top-to-bottom

  • Has easier debugging (looks like synchronous execution)

  • Has Cleaner error handling with try...catch

It helps you focus on what the code is doing, not how the async flow is managed.

PROMISES (chain)              ASYNC/AWAIT (linear)

start                         start
  ↓                             ↓
then(step1)                  await step1
  ↓                             ↓
then(step2)                  await step2
  ↓                             ↓
then(step3)                  await step3
  ↓                             ↓
catch(error)                 try...catch

Conclusion

Async await is not a something new but a wrapper around Promises, they make development and debugging faster than ever, This helps in maintaining large code bases, because human eyes are comfortable understanding synchronous actions where each line of code gets executed one after another.

J
Jimmy1mo ago

Nice explanation 👍 async/await makes Promises much easier to read and manage, especially for beginners.

It would be great if you also add more examples for error handling using try/catch.

I also found a simple reference here: https://sharexzone.com