Skip to main content

Async/Await in JavaScript: Writing Cleaner Asynchronous Code

Updated 7 min readMarkdown

Introduction

JavaScript is single-threaded, but modern applications constantly perform asynchronous tasks like fetching APIs, reading files, handling databases, and waiting for timers. Without asynchronous programming, every operation would block the entire application like a traffic jam caused by one confused auto-rickshaw driver stopping in the middle of the road.

Initially, JavaScript developers used callbacks for async operations. Then promises arrived and improved the situation. After that, async/await entered like a cleanup crew removing the spaghetti cables hanging from the ceiling.

In this article, we will understand:

  • Why async/await was introduced

  • How async functions work

  • The await keyword

  • Error handling in async code

  • Comparison between promises and async/await

  • Why async/await improves readability


What is Asynchronous Programming?

Asynchronous programming allows JavaScript to execute long-running tasks without blocking the main thread.

For example:

  • Fetching data from an API

  • Reading files

  • Waiting for a timer

  • Database queries

  • Authentication requests

Instead of freezing the application while waiting, JavaScript continues executing other code.

Simple Example

console.log("Start");

setTimeout(() => {
    console.log("Task completed");
}, 2000);

console.log("End");

Output

Start
End
Task completed

Even though the timeout appears in the middle, JavaScript does not wait for it.


Problems Before Async/Await

Before promises and async/await, developers mainly used callbacks.

Callback Example

getUser(function(user) {
    getPosts(user.id, function(posts) {
        getComments(posts[0].id, function(comments) {
            console.log(comments);
        });
    });
});

This deeply nested structure is called Callback Hell.

Problems with callback hell:

  • Difficult to read

  • Hard to debug

  • Hard to maintain

  • Error handling becomes messy

  • Code starts looking like a collapsing staircase

Promises improved this.


Promises in JavaScript

A promise represents a value that may be available now, later, or never.

Promise Example

fetch("https://jsonplaceholder.typicode.com/users/1")
    .then((response) => response.json())
    .then((data) => {
        console.log(data);
    })
    .catch((error) => {
        console.log(error);
    });

Promises solved many callback problems, but chaining multiple .then() methods could still reduce readability.

That is where async/await comes in.


Why Async/Await Was Introduced

Async/await was introduced to make asynchronous code:

  • Cleaner

  • Easier to read

  • Easier to write

  • Easier to debug

  • More similar to synchronous code

It is basically syntactic sugar over promises.

That means async/await does not replace promises internally. It simply provides a cleaner syntax for working with them.

Think of promises as raw engine components and async/await as the polished dashboard interface.


How Async Functions Work

An async function always returns a promise.

Syntax

async function functionName() {
    // async code
}

Example

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

greet().then((data) => {
    console.log(data);
});

Output

Hello

Even though we returned a string, JavaScript automatically wraps it inside a promise.

Equivalent behavior:

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

Understanding the Await Keyword

The await keyword pauses execution of an async function until a promise resolves.

It can only be used inside an async function.

Example

async function fetchData() {
    let response = await fetch("https://jsonplaceholder.typicode.com/users/1");

    let data = await response.json();

    console.log(data);
}

fetchData();

What Happens Internally?

  1. fetch() returns a promise

  2. await pauses the function

  3. JavaScript continues other tasks

  4. When the promise resolves, execution resumes

  5. Data gets stored in response

This makes async code look synchronous without blocking the main thread.

That’s the important part many beginners misunderstand.

await pauses only the async function, not the entire JavaScript engine. Otherwise every website using async/await would behave like a frozen PowerPoint presentation.


Promise vs Async/Await Comparison

Using Promises

fetch("https://jsonplaceholder.typicode.com/users/1")
    .then((response) => response.json())
    .then((data) => {
        console.log(data);
    })
    .catch((error) => {
        console.log(error);
    });

Using Async/Await

async function getUser() {
    try {
        let response = await fetch("https://jsonplaceholder.typicode.com/users/1");

        let data = await response.json();

        console.log(data);
    } catch (error) {
        console.log(error);
    }
}

getUser();

Which Looks Cleaner?

The async/await version is usually:

  • More readable

  • Easier to maintain

  • Easier to debug

  • Easier for beginners to understand

Promises are still important because async/await is built on top of them.

If you do not understand promises, async/await will eventually humble you during debugging.


Error Handling with Async Code

One of the biggest advantages of async/await is cleaner error handling using try...catch.

Example

async function fetchUser() {
    try {
        let response = await fetch("https://wrong-url.com");

        let data = await response.json();

        console.log(data);
    } catch (error) {
        console.log("Error:", error.message);
    }
}

fetchUser();

Why This is Better

Without async/await, handling errors in long promise chains can become difficult.

With async/await:

  • Errors look like normal synchronous exceptions

  • Debugging becomes simpler

  • Code becomes cleaner


Async/Await Execution Flow

Flow Explanation

Call async function
        ↓
Function starts execution
        ↓
await encountered
        ↓
Function pauses temporarily
        ↓
JavaScript continues other tasks
        ↓
Promise resolves/rejects
        ↓
Function resumes execution
        ↓
Result returned

Promise Flow vs Async/Await Flow

Promise Flow

Promise Created
      ↓
.then()
      ↓
.then()
      ↓
.catch()

Async/Await Flow

async function starts
        ↓
await promise
        ↓
result received
        ↓
try/catch handles errors

Async/await reduces visual complexity significantly.

Less mental gymnastics. Fewer chained brackets trying to impersonate abstract art.


Important Things to Remember

1. Async Functions Always Return Promises

async function test() {
    return 10;
}

Actually returns:

Promise { 10 }

2. Await Works Only Inside Async Functions

Wrong:

let data = await fetch(url);

Correct:

async function getData() {
    let data = await fetch(url);
}

3. Await Does Not Block Entire JavaScript

Only the current async function pauses.

JavaScript can still:

  • Handle clicks

  • Run timers

  • Execute other code

  • Render UI


4. Async/Await Still Uses Promises Internally

This is extremely important.

Many beginners think async/await replaced promises completely.

Wrong.

Async/await is just a cleaner way to work with promises.


Real World Example

Fetching API Data

async function getPosts() {
    try {
        let response = await fetch("https://jsonplaceholder.typicode.com/posts");

        let posts = await response.json();

        console.log(posts);
    } catch (error) {
        console.log("Failed to fetch posts");
    }
}

getPosts();

This style is heavily used in:

  • React applications

  • Node.js backend development

  • Express APIs

  • Database operations

  • Authentication systems


Advantages of Async/Await

Cleaner Syntax

Code looks simpler and easier to follow.

Better Readability

Async code appears almost synchronous.

Easier Error Handling

try...catch works naturally.

Easier Debugging

Stack traces are easier to understand.

Better Maintainability

Less nested code.


Limitations of Async/Await

Async/await is not magic.

Some developers start using it everywhere like sprinkling oregano on every food item.

That creates problems too.

Sequential Await Problem

let a = await task1();
let b = await task2();

These run one after another.

Sometimes parallel execution is faster.

Better approach:

let [a, b] = await Promise.all([task1(), task2()]);

Understanding when to use sequential vs parallel execution is important for performance.


Conclusion

Async/await made asynchronous JavaScript cleaner and more readable.

Instead of deeply nested callbacks or long promise chains, developers can write async code in a style that looks close to synchronous programming.

Key takeaways:

  • Async/await is syntactic sugar over promises

  • Async functions always return promises

  • await pauses only the current async function

  • try...catch simplifies error handling

  • Async/await improves readability significantly

  • Promises are still the foundation underneath

Modern JavaScript development heavily depends on async programming.

Whether you work with React, Node.js, APIs, databases, or authentication systems, understanding async/await is mandatory.

Ignoring async concepts in JavaScript is like trying to become a gym athlete while skipping protein entirely. Technically possible. Practically painful.

No comments yet. Be the first to comment.