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
P

Software Engineer | Passionate about Web Development, DSA & Problem Solving. I write simple, practical tech blogs to help developers learn and grow. Exploring JavaScript, C++, Backend & Modern Web Technologies.

Introduction

Asynchronous programming is a core part of JavaScript, allowing applications to perform tasks like API calls, file handling, and timers without blocking execution.

Before async/await, developers primarily used callbacks and later Promises to manage asynchronous code. While powerful, these approaches often made code harder to read and maintain, especially when dealing with multiple dependent operations.

To solve this problem, JavaScript introduced async/await, a cleaner and more readable way to handle asynchronous operations. It is essentially syntactic sugar over Promises, meaning it uses Promises internally but allows developers to write code that looks synchronous.

1. Why Async/Await Was Introduced

Callbacks led to deeply nested code (callback hell), and Promises improved the situation but still required chaining using .then().

Async/await was introduced to:

  • Improve readability

  • Reduce complexity

  • Make asynchronous code look synchronous

  • Simplify error handling

Example (Using Promises):

fetch("https://api.example.com/data")
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.log(error));

Explanation :

  • fetch() returns a Promise

  • .then() is used to handle success

  • .catch() handles errors

  • Multiple .then() calls create a chain

This works, but readability decreases as logic grows.

Same Example Using Async/Await:

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

Explanation :

  • async makes the function return a Promise

  • await pauses execution until Promise resolves

  • Code looks sequential (top to bottom)

  • try/catch handles errors cleanly

This is much easier to read and maintain.

2. How Async Functions Work

An async function always returns a Promise.

Example:

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

greet().then(console.log);

Explanation :

  • Even though we return a string, it is wrapped in a Promise

  • So internally it behaves like:

Promise.resolve("Hello World");

Output:

Hello World

This means async functions always return a Promise, whether you explicitly return one or not.

3. Await Keyword

The await keyword is used to pause execution until a Promise is resolved.

Example:

function delay() {
    return new Promise(resolve => {
        setTimeout(() => resolve("Done"), 2000);
    });
}

async function run() {
    console.log("Start");
    const result = await delay();
    console.log(result);
    console.log("End");
}

run();

Explanation :

  • delay() returns a Promise that resolves after 2 seconds

  • await delay() pauses execution of run()

  • JavaScript waits for Promise to resolve

  • Then execution continues

Output:

Start
Done
End

Important:

  • await only works inside async functions

  • It does not block the entire program, only that function

4. Error Handling with Async Code

Error handling becomes cleaner with try/catch.

Example:

async function fetchData() {
    try {
        const response = await fetch("invalid-url");
        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.log("Error occurred:", error.message);
    }
}

fetchData();

Explanation :

  • Invalid URL causes Promise rejection

  • await throws an error

  • catch block handles it

Output:

Error occurred: Failed to fetch

This is cleaner compared to chaining .catch().

5. Comparison with Promises

Both Promises and async/await handle asynchronous operations, but async/await improves readability.

Promise Style:

doTask()
    .then(result => nextTask(result))
    .then(final => console.log(final))
    .catch(err => console.log(err));

Async/Await Style:

async function runTasks() {
    try {
        const result = await doTask();
        const final = await nextTask(result);
        console.log(final);
    } catch (err) {
        console.log(err);
    }
}

Explanation:

  • Promise chaining uses .then() repeatedly

  • Async/await uses simple sequential code

  • Easier to read, debug, and maintain

6. Async Function Execution Flow

Understanding execution flow helps in mastering async behavior.

Example:

async function test() {
    console.log("A");
    await Promise.resolve();
    console.log("B");
}

test();
console.log("C")Explanation of Example:

Execution order:

  1. A prints

  2. await pauses function

  3. C prints (outside function)

  4. Then B prints

Output:

A
C
B

This shows:

  • await pauses only inside function

  • Event loop handles execution order

Conclusion

Async/await is one of the most powerful features in modern JavaScript for handling asynchronous operations.

It simplifies code by:

  • Removing complex .then() chains

  • Making code look synchronous

  • Improving readability and maintainability

Since it is built on top of Promises, understanding both concepts together provides a strong foundation for asynchronous programming.