Learnitweb

async in JavaScript

According to the definition, async is a keyword that allows an asynchronous function to be written in a way that looks synchronous.

But let’s not focus on definitions too much.
Instead, let’s understand what actually happens.

When you place the async keyword before a function, JavaScript automatically makes that function return a promise.

Even if you return a simple value like a string or a number, JavaScript will automatically wrap it inside a resolved promise.

Let’s look at a simple example.

async function sayHello() {
    return "Hello, World!";
}

Now if we call this function:

const result = sayHello();
console.log(result);

You might expect "Hello, World", but instead you’ll see something like:

Promise { <fulfilled>: "Hello, World!" }

This happens because every async function always returns a promise.

Even though we returned a simple string, JavaScript automatically wrapped it inside a resolved promise.

Returning a promise explicitly

Now let’s return an actual promise from an async function.

async function getMessage() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve("Hello after 10 seconds");
        }, 10000);
    });
}

If we call this function:

const result = getMessage();
console.log(result);

Initially, you’ll see a pending promise.

After 10 seconds, that promise becomes fulfilled with the value:

"Hello after 10 seconds"

In this case, JavaScript does not wrap anything.
It simply returns the promise as-is.

Returning a rejected promise

Async functions can also return rejected promises.

async function getError() {
    return Promise.reject("Something went wrong");
}

If we call it:

getError().catch(err => console.log(err));

You’ll see:

Something went wrong

Again, JavaScript does not modify the promise — it just returns it.

So far, we’ve learned something important:

An async function always returns a promise.
If the returned value is not a promise, JavaScript wraps it automatically.

Introducing await

Now let’s talk about the await keyword.

await is used inside an async function to pause execution until a promise settles — whether it resolves or rejects.

This does not block the entire application.
Only the current async function pauses.
The rest of JavaScript continues running normally.


Example: waiting for a value

Let’s create a function that returns a number after a delay.

function getNumber() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(42);
        }, 2000);
    });
}

Now we’ll use await to get this value.

async function printNumber() {
    const number = await getNumber();
    console.log(number);
}

When you call:

printNumber();

You’ll notice that:

  • Nothing prints immediately
  • After 2 seconds, 42 appears in the console

What’s happening is that the function pauses at the await line until the promise resolves.

But importantly — the JavaScript engine is not blocked. Other code can continue running.


What await really does

Behind the scenes, this:

const number = await getNumber();

Is equivalent to writing:

getNumber().then(number => {
    console.log(number);
});

So async/await is really just syntactic sugar over promises.
It makes asynchronous code easier to read and reason about.


Using async/await with Fetch API

Now let’s look at a more realistic example using the fetch API.

Suppose you want to get a random dog image from an API.

Using traditional promises:

fetch("https://dog.ceo/api/breeds/image/random")
    .then(response => response.json())
    .then(data => {
        console.log(data.message);
    });

This works, but it’s slightly harder to read when the logic grows.


The same example using async/await

async function getRandomDog() {
    const response = await fetch("https://dog.ceo/api/breeds/image/random");
    const data = await response.json();
    console.log(data.message);
}

When you call:

getRandomDog();

You get the same result — a random dog image URL.

The difference is readability.
The async version looks almost like synchronous code, even though it’s fully asynchronous.

Using await outside of an async function

Let’s start with something important.

You already know that the await keyword can only be used inside an async function.
If you try to use it inside a normal function, JavaScript throws a syntax error.

For example, this will not work:

function test() {
    const result = await Promise.resolve(10);
    console.log(result);
}

You’ll get an error saying something like:

SyntaxError: await is only valid in async functions

This usually means you forgot to mark the function as async.


Using await at the top level

Now here’s something interesting.

In the browser console, you can actually write:

const value = await Promise.resolve(42);
console.log(value);

And it works.

But if you try the same thing in Node.js, you’ll get an error — unless you configure your environment correctly.


Why does this happen?

By default, Node.js does not allow await at the top level unless the file is treated as an ES module.

Let’s see what happens.


Example: using await at top level in Node.js

Suppose we have this code:

const result = await Promise.resolve("Hello");
console.log(result);

If you run this in Node.js (without any setup), you’ll get an error saying:

await is only valid in async functions and the top level bodies of modules

So how do we fix this?


Option 1: Wrap the code inside an async function

This is the most common and safest approach.

(async function () {
    const result = await Promise.resolve("Hello");
    console.log(result);
})();

Now when you run this, it works perfectly.


Option 2: Enable top-level await using ES modules

Node.js supports top-level await starting from version 14.8, but only when using ES modules.

There are two ways to enable this.


Option A: Rename the file to .mjs

If your file is named:

index.mjs

Then Node.js automatically treats it as an ES module.

Now this works:

const result = await Promise.resolve("Hello");
console.log(result);

No async wrapper required.


Option B: Use type: "module" in package.json

You can also enable ES modules by adding this to your package.json:

{
  "type": "module"
}

Now all .js files inside that project can use top-level await.


Handling errors with async / await

Now let’s talk about error handling.

When you await a promise and that promise rejects, JavaScript throws an error — just like a normal exception.

That means we can use try...catch.


Example: handling errors using try–catch

async function fetchData() {
    try {
        const response = await fetch("https://invalid-url.com");
        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.log("Something went wrong:", error.message);
    }
}

If the request fails, the control jumps straight into the catch block.

This is one of the biggest advantages of async/await:
error handling feels natural and readable.


Handling errors without try–catch

Since async functions always return promises, we can also handle errors using .catch().

async function fetchData() {
    const response = await fetch("https://invalid-url.com");
    const data = await response.json();
    return data;
}

fetchData()
    .then(data => console.log(data))
    .catch(error => console.log("Error:", error.message));

Both approaches are valid — choose whichever fits your style.


Executing async functions one by one (sequential execution)

Now let’s talk about executing multiple async operations one after another.

First, we’ll create two async functions:

function printOne() {
    return new Promise(resolve => {
        setTimeout(() => resolve("One"), 1000);
    });
}

function printTwo() {
    return new Promise(resolve => {
        setTimeout(() => resolve("Two"), 1000);
    });
}

Now let’s execute them sequentially:

async function oneByOne() {
    const first = await printOne();
    console.log(first);

    const second = await printTwo();
    console.log(second);
}

When you run this, you’ll see:

  • "One" after 1 second
  • "Two" after another second

Total time: about 2 seconds.


Executing async functions in parallel

Now let’s do the same thing, but in parallel.

async function inParallel() {
    const p1 = printOne();
    const p2 = printTwo();

    const result1 = await p1;
    const result2 = await p2;

    console.log(result1, result2);
}

This time, both functions start immediately.

The total time is now only 1 second, not 2.


Cleaner version using destructuring

You can also write this more cleanly:

async function inParallel() {
    const [one, two] = await Promise.all([
        printOne(),
        printTwo()
    ]);

    console.log(one, two);
}

This does exactly the same thing and is often easier to read.


Key idea to remember

  • Using await one after another → sequential execution
  • Starting promises first and then awaiting → parallel execution

Both approaches are valid — it depends on what you need.