Skip to content
Back to blogs
April 11, 2026·Updated April 11, 2026·6 min read

Async/Await in JavaScript: Writing Cleaner Asynchronous Code

Learn async/await in JavaScript with beginner-friendly examples. Understand async functions, await, error handling, promises, and cleaner asynchronous code.

Summary

Async/await gives JavaScript developers a cleaner way to work with asynchronous code without abandoning promises. This blog explains why it was introduced, how `async` functions and `await` actually behave, how to handle errors properly, and where it fits compared to plain promise chains.

Async/Await in JavaScript: Writing Cleaner Asynchronous Code

Imagine ordering food at a restaurant that gives you a buzzer. You place the order, sit down, and continue your conversation while the kitchen works. When the food is ready, the buzzer goes off and you pick it up. That is how asynchronous JavaScript feels: you start a task, move on, and come back when the result is ready. async/await did not change that system. It simply gave us a much cleaner way to describe it.

Why Async/Await Was Introduced

Before async/await, JavaScript developers mostly handled asynchronous code with callbacks and then with promises. Promises were already a big improvement because they flattened deeply nested callback code and centralized error handling. But once promise chains became long, the code still started to feel mechanical.

Here is a common promise-based flow:

fetchUser()
  .then((user) => fetchPosts(user.id))
  .then((posts) => fetchComments(posts[0].id))
  .then((comments) => {
    console.log(comments);
  })
  .catch((error) => {
    console.error("Something failed:", error);
  });

That is valid code. It is not wrong. But mentally, you have to keep tracking what each .then() returns and how values move down the chain.

Now look at the same logic with async/await:

async function showComments() {
  try {
    const user = await fetchUser();
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);
 
    console.log(comments);
  } catch (error) {
    console.error("Something failed:", error);
  }
}

This is why async/await was introduced in ES2017: not to replace promises, but to make promise-based code read more like normal step-by-step logic.

PROMISE CHAIN                        ASYNC/AWAIT
─────────────                        ───────────
 
fetchUser()                          const user = await fetchUser()
  .then(user =>                      const posts = await fetchPosts(user.id)
    fetchPosts(user.id))             const comments = await fetchComments(...)
  .then(posts =>                     console.log(comments)
    fetchComments(...))
  .then(comments =>
    console.log(comments))
  .catch(handleError)
 
Same promise engine underneath.
Cleaner top-to-bottom reading on the right.

That is why people call async/await syntactic sugar. The engine is still dealing with promises. The syntax just gives us a friendlier way to express the same asynchronous behavior.

How Async Functions Work

Think of an async function like a gift box around a value. Even if you return a plain value, JavaScript wraps it in a promise for you.

async function getMessage() {
  return "Hello";
}
 
getMessage().then((value) => {
  console.log(value); // Hello
});

Even though we returned a string, getMessage() does not return the string directly. It returns Promise.resolve("Hello") under the hood.

That leads to one of the most important rules in this topic:

Every async function always returns a promise.

If the function returns a value, the promise is fulfilled with that value. If the function throws an error, the promise is rejected.

async function getUser() {
  throw new Error("User not found");
}
 
getUser().catch((error) => {
  console.error(error.message); // User not found
});

Here is the execution model:

┌──────────────────────────────────────────────┐
│        ASYNC FUNCTION EXECUTION FLOW         │
├──────────────────────────────────────────────┤
│ 1. Call async function                       │
│ 2. Function immediately returns a Promise    │
│ 3. Body starts running                       │
│ 4. If it hits await, function pauses itself  │
│ 5. Other JS work can continue                │
│ 6. When awaited promise settles, resume      │
│ 7. Resolve or reject final returned promise  │
└──────────────────────────────────────────────┘

For intermediate developers, this is the key mental model: await does not block the whole JavaScript engine. It only pauses the execution of that specific async function until the awaited promise settles.

What the Await Keyword Really Does

await is like telling JavaScript, "pause this function here until that promise gives me a result." The important phrase is this function. Not the browser. Not Node.js. Not the entire call stack forever.

async function getData() {
  console.log("Start");
 
  const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  const data = await response.json();
 
  console.log(data);
  console.log("End");
}
 
getData();
console.log("I run before the fetch finishes");

The last console.log() runs before the network response comes back, because await pauses getData, not the entire program.

Main script:
  call getData()
  print "I run before the fetch finishes"
 
Inside getData():
  print "Start"
  await fetch(...)
  pause function
  ...later resume...
  await response.json()
  print data
  print "End"

There are also two common doubts worth clearing up:

  1. await usually waits for a promise, but if you give it a non-promise value, JavaScript wraps that value with Promise.resolve(...).
  2. await can only be used inside an async function, unless you are using top-level await inside an ES module.

Here is a tiny example of non-promise behavior:

async function demo() {
  const value = await 42;
  console.log(value); // 42
}

You usually would not write code like that, but it helps explain what await is doing internally.

Error Handling with Async Code

Handling async errors is one of the biggest reasons developers love async/await. With promise chains, you often attach a .catch() at the end. With async/await, you can use try/catch, which feels closer to normal synchronous error handling.

async function loadProfile() {
  try {
    const response = await fetch("/api/profile");
 
    if (!response.ok) {
      throw new Error("Failed to load profile");
    }
 
    const profile = await response.json();
    console.log(profile);
  } catch (error) {
    console.error("Request failed:", error.message);
  } finally {
    console.log("Cleanup, hide spinner, or close connection");
  }
}

That structure gives you three clear zones:

  • try: the work that might fail
  • catch: what to do when it fails
  • finally: cleanup that should happen either way
┌───────────┐
│   try     │
└─────┬─────┘
      │
      ▼
  await work
      │
  ┌───┴────┐
  │        │
  ▼        ▼
success   error thrown
  │        │
  ▼        ▼
continue  catch block
  │        │
  └───┬────┘
      ▼
   finally

One advanced but important detail: fetch() only rejects on network-level failures. A 404 or 500 response does not automatically throw. You still need to check response.ok yourself if you want to treat bad HTTP responses as errors.

Another common mistake is forgetting to await a promise inside try/catch. If you return a promise without awaiting it, the rejection may escape that catch block.

async function wrong() {
  try {
    return fetch("/api/data");
  } catch (error) {
    console.log("This will not catch fetch rejection reliably");
  }
}
 
async function right() {
  try {
    return await fetch("/api/data");
  } catch (error) {
    console.log("This catch can handle the awaited rejection");
  }
}

Async/Await vs Promises

This comparison is where many beginners get confused, so let us state it directly: async/await is built on top of promises. It is not a separate async system.

Use promise chains when:

  • you want quick one-off transformations
  • you are composing values in a chain-heavy style
  • you are working with APIs that already expose .then() cleanly

Use async/await when:

  • you want step-by-step readability
  • you need local variables across multiple async steps
  • you want try/catch/finally style error handling

Here is a side-by-side example:

// Promise style
fetch("/api/user")
  .then((response) => response.json())
  .then((user) => {
    console.log(user.name);
  })
  .catch((error) => {
    console.error(error);
  });
 
// Async/await style
async function showUser() {
  try {
    const response = await fetch("/api/user");
    const user = await response.json();
    console.log(user.name);
  } catch (error) {
    console.error(error);
  }
}

For most application code, async/await is easier to scan because it reads top to bottom. For library code or functional composition, plain promises can still be elegant.

Common Doubts and Real-World Advice

One mistake I see beginners make is assuming await automatically makes code optimal. It does not. If you await independent tasks one by one, you may slow things down.

async function slow() {
  const user = await fetchUser();
  const settings = await fetchSettings();
  return { user, settings };
}
 
async function faster() {
  const [user, settings] = await Promise.all([fetchUser(), fetchSettings()]);
 
  return { user, settings };
}

In slow(), the second request starts only after the first one finishes. In faster(), both start together.

SEQUENTIAL AWAITS                PARALLEL WITH Promise.all
──────────────────               ─────────────────────────
 
fetchUser()                      fetchUser() ──┐
   wait                             fetchSettings() ──┐
fetchSettings()                    wait for both      │
   wait                                               │
done                              done <──────────────┘

Another good practice: avoid mixing .then() and await in the same function unless there is a strong reason. It is usually harder to read.

Also remember this important truth: async code is not automatically faster. async/await improves structure and readability. It does not make CPU-heavy code execute faster. It mainly helps you manage waiting for I/O like network calls, timers, and file operations.

Key Takeaways

async/await was introduced to make promise-based asynchronous JavaScript easier to read and maintain. An async function always returns a promise, and await pauses only that function until a promise settles. This gives you code that looks synchronous while still behaving asynchronously.

The best way to think about it is simple: promises are the engine, async/await is the cleaner dashboard. If you understand both layers, your code becomes easier to write, easier to debug, and much easier to explain to the next developer reading it.