Skip to main content

Command Palette

Search for a command to run...

Async/Await in JavaScript: Writing Cleaner Asynchronous Code

Updated
4 min read

If you have been learning JavaScript, you know that dealing with things that take time—like fetching data from an API or reading a file—can get messy.

First, we had Callbacks, which quickly turned into an unreadable mess known as "Callback Hell." To fix that, JavaScript introduced Promises, which were much better, but chaining .then() and .catch() over and over still felt a little clunky.

Finally, JavaScript gave us Async/Await. It is the modern, cleanest way to handle asynchronous operations. Let’s break down how it works and why you should be using it.

What is Async/Await? (The "Syntactic Sugar")

Here is the most important secret about async/await: It is entirely built on top of Promises.

It doesn't introduce a new way of doing things under the hood. Developers call it "syntactic sugar" because it simply provides a sweeter, cleaner, and more readable syntax for working with Promises.

Its main goal is to make asynchronous code look and behave like normal, synchronous code.

1. The async Keyword

The async keyword does one simple thing: it transforms a regular function into an asynchronous function. You just place it right in front of the function declaration.

When you declare a function as async, JavaScript automatically ensures that the function always returns a Promise, even if you don't explicitly write one.

// A normal function
function sayHello() {
    return "Hello there!";
}

// An async function
async function sayHelloAsync() {
    return "Hello there!";
}

console.log(sayHelloAsync()); 
// Output: Promise { 'Hello there!' }

Because it returns a Promise, you could use .then() on it, but there is a much better way.

2. The await Keyword

The await keyword is where the real magic happens. It can only be used inside an async function.

When JavaScript sees the await keyword, it literally pauses the execution of that specific function until the Promise finishes (resolves or rejects). Meanwhile, the rest of your application keeps running smoothly in the background.

async function fetchUserData() {
    console.log("Fetching user...");

    // The code stops here and waits for the data to arrive
    const response = await fetch('https://api.example.com/user');
    const data = await response.json();

    console.log("User data loaded:", data);
}

Notice how there are no callbacks and no .then() chains? It reads top-to-bottom, exactly like synchronous code.

3. Comparison: Promises vs. Async/Await

To truly appreciate async/await, look at how much it improves readability compared to traditional Promises.

Let's imagine we have a function called getUserData() that returns a Promise.

The Old Way (Using Promises):

function loadUser() {
    getUserData()
        .then(user => {
            console.log("User found:", user);
            return getPosts(user.id); // Returning another promise
        })
        .then(posts => {
            console.log("User posts:", posts);
        })
        .catch(error => {
            console.log("Something went wrong:", error);
        });
}

The Modern Way (Using Async/Await):

async function loadUser() {
    // Readability is instantly improved!
    const user = await getUserData();
    console.log("User found:", user);

    const posts = await getPosts(user.id);
    console.log("User posts:", posts);
}

The async/await version removes all the nested logic and callback functions. You just declare your variables and wait for the data.

4. Error Handling (Try / Catch)

In the Promise example above, we used .catch() to handle errors. But with async/await, we can go back to using the standard JavaScript try...catch blocks.

Because await pauses the code, if a Promise rejects (fails), it will throw an error right on that line. The catch block catches it immediately.

async function fetchUserProfile() {
    try {
        console.log("Starting fetch...");
        
        // If this URL is broken, it throws an error immediately
        const response = await fetch('https://api.broken-link.com/profile');
        const data = await response.json();
        
        console.log("Success:", data);

    } catch (error) {
        // We handle the failure gracefully here
        console.error("Network Error:", error.message);
    } finally {
        console.log("Request finished (either succeeded or failed).");
    }
}

Using try/catch is incredibly powerful because it allows you to handle both synchronous errors (like a typo in your code) and asynchronous errors (like a broken API) in the exact same way.

Summary

  1. async/await is just syntactic sugar over Promises. It makes your code cleaner.

  2. The async keyword makes a function always return a Promise.

  3. The await keyword pauses the function until the Promise is complete, but without freezing your app.

  4. It makes asynchronous code read like standard, top-to-bottom synchronous code.

  5. Always use try...catch blocks with async/await to safely handle errors!

Stay Tune for next Blog! Happy Coding!

More from this blog

Web Devlopment Journey

48 posts

In this publication, you will find articles related to web devlopment whether it be git or node.js. There will be blogs on every single topic and more will be coming soon...