Skip to content
Home
JavaScript Async/Await: Understanding Asynchronous Programming

JavaScript Async/Await: Understanding Asynchronous Programming

JavaScript JavaScript 7 min read 1429 words Beginner ExcellentWiki Editorial Team

JavaScript runs on a single thread, but the world is asynchronous. Network requests, file reads, timers, and user input all happen outside the main execution flow. If JavaScript waited for each of these operations to complete before moving on, the UI would freeze and the user experience would suffer.

Async/await is the modern syntax for writing asynchronous code that looks and behaves like synchronous code. It is built on top of promises, but it eliminates the nested callbacks and chained .then() calls that made asynchronous JavaScript hard to read and debug.

The Problem Async/Await Solves

Before async/await, asynchronous code used callbacks:

function fetchUser(id, callback) {
  setTimeout(() => {
    callback({ id, name: "Alice" });
  }, 1000);
---

function fetchPosts(userId, callback) {
  setTimeout(() => {
    callback([{ id: 1, title: "Post 1" }]);
  }, 1000);
---

// Callback hell
fetchUser(1, (user) => {
  fetchPosts(user.id, (posts) => {
    console.log(user, posts);
    // Nesting gets worse with each additional step
  });
---);

Promises improved the situation with chaining:

fetchUser(1)
  .then(user => fetchPosts(user.id))
  .then(posts => console.log(posts))
  .catch(err => console.error(err));

Async/await makes it read like synchronous code:

async function loadUserProfile(userId) {
  const user = await fetchUser(userId);
  const posts = await fetchPosts(user.id);
  console.log(user, posts);
---

Understanding Promises

Async/await is syntactic sugar over promises. A promise represents a value that may be available now, later, or never:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Done!");
    // or reject(new Error("Failed"));
  }, 1000);
---);

promise.then(result => console.log(result));
promise.catch(err => console.error(err));

A promise has three states:

  • Pending — initial state, neither fulfilled nor rejected
  • Fulfilled — the operation completed successfully
  • Rejected — the operation failed

Once settled (fulfilled or rejected), a promise cannot change state.

Async Functions

An async function always returns a promise. If the function returns a value, the promise resolves with that value. If it throws, the promise rejects:

async function greet(name) {
  return `Hello, ${name}!`;
---

greet("Alice").then(result => console.log(result)); // "Hello, Alice!"

async function fail() {
  throw new Error("Oops");
---

fail().catch(err => console.log(err.message)); // "Oops"

This means every async function is thenable — you can use .then() and .catch() on its return value.

The Await Keyword

await pauses the execution of the async function until the promise settles. It then returns the fulfilled value or throws the rejection reason:

async function getData() {
  const response = await fetch("https://api.example.com/data");
  const data = await response.json();
  return data;
---

Each await line pauses the function until that promise resolves. This makes the code read top-to-bottom like synchronous code, but the function itself runs asynchronously — it does not block the main thread.

Await and Non-Promise Values

If you await a non-promise value, it is wrapped in a resolved promise:

async function example() {
  const result = await 42;       // wraps in Promise.resolve(42)
  console.log(result);           // 42
---

Top-Level Await

In modern JavaScript modules, you can use await outside of an async function:

// In a module
const response = await fetch("/api/config");
const config = await response.json();
export default config;

Modules that use top-level await block their importing modules from executing until they resolve, which simplifies initialization logic.

Error Handling

Try/Catch

The standard way to handle errors in async functions is try/catch:

async function fetchUser(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error("Failed to fetch user:", error);
    return null; // graceful fallback
  }
---

A single try/catch around multiple awaits catches any rejection:

async function loadDashboard() {
  try {
    const user = await fetchUser(1);
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);
    return { user, posts, comments };
  } catch (error) {
    showError(error.message);
  }
---

Catching Outside the Async Function

Errors propagate up through the returned promise:

async function risky() {
  throw new Error("Boom");
---

// Catch outside
risky().catch(err => console.error(err));

// Or with try/catch in the caller
async function caller() {
  try {
    await risky();
  } catch (err) {
    console.error(err);
  }
---

Parallel Execution

One common mistake is awaiting sequential operations that could run in parallel:

// Slow — runs sequentially
async function slow() {
  const user = await fetchUser(1);
  const settings = await fetchSettings(1);
  return { user, settings };
---

Run independent promises in parallel with Promise.all:

// Fast — runs in parallel
async function fast() {
  const [user, settings] = await Promise.all([
    fetchUser(1),
    fetchSettings(1)
  ]);
  return { user, settings };
---

Promise.all rejects immediately if any promise rejects. For scenarios where you want all results regardless of individual failures, use Promise.allSettled:

const results = await Promise.allSettled([
  fetchUser(1),
  fetchSettings(1)
]);

results.forEach(result => {
  if (result.status === "fulfilled") {
    console.log("Success:", result.value);
  } else {
    console.log("Failed:", result.reason);
  }
---);

Promise.race and Promise.any

Promise.race settles with the first promise that settles (resolve or reject). Useful for timeouts:

async function fetchWithTimeout(url, ms = 5000) {
  const response = await Promise.race([
    fetch(url),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error("Timeout")), ms)
    )
  ]);
  return response.json();
---

Promise.any resolves with the first fulfilled promise and ignores rejections unless all reject:

const fastest = await Promise.any([
  fetchFromCDN("data.json"),
  fetchFromOrigin("data.json")
]);

Common Patterns

Sequential Loops

When each iteration depends on the previous one, use a for-of loop:

async function processItems(items) {
  const results = [];
  for (const item of items) {
    const result = await processItem(item);  // sequential
    results.push(result);
  }
  return results;
---

Parallel Mapped Operations

When iterations are independent, map them to promises and await all:

async function fetchAllUsers(userIds) {
  const promises = userIds.map(id => fetch(`/api/users/${id}`).then(r => r.json()));
  return Promise.all(promises);
---

Async Array Methods

Array methods like map, filter, and reduce do not work with async callbacks the way you might expect. Array.prototype.map returns an array of promises, not resolved values. Use Promise.all after mapping:

const results = await Promise.all(items.map(async (item) => {
  const data = await fetch(`/api/${item.id}`);
  return data.json();
---));

For async filter, map first then filter the resolved pairs:

async function asyncFilter(array, predicate) {
  const results = await Promise.all(array.map(predicate));
  return array.filter((_, index) => results[index]);
---

const expensiveItems = await asyncFilter(items, async (item) => {
  const price = await fetchPrice(item.id);
  return price > 100;
---);

Retry with Backoff

async function fetchWithRetry(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (error) {
      if (i === retries - 1) throw error;
      await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
    }
  }
---

Async/Await vs Raw Promises

Async/await is not a replacement for promises — it is built on them. There are cases where using promises directly is still appropriate:

// Fire and forget — no need to await
function logEvent(event) {
  fetch("/api/log", { method: "POST", body: JSON.stringify(event) })
    .catch(err => console.warn("Log failed:", err));
---

// Per-task handling with different fallbacks
fetchUser(1)
  .then(renderProfile)
  .catch(showProfileError);

fetchSettings(1)
  .then(renderSettings)
  .catch(showSettingsError);

For most other cases, async/await produces cleaner, more readable, and easier-to-debug code. Stack traces from async/await are usually clearer than promise chains, especially when using source maps.

Summary

Async/await transforms asynchronous JavaScript into readable, synchronous-looking code. async functions always return promises, await pauses execution until a promise settles, and try/catch handles errors gracefully. Use Promise.all for parallel independent operations, Promise.race for timeouts, and Promise.allSettled when you want all results regardless of failures. Understanding both promises and async/await gives you the full toolkit for handling asynchrony in modern JavaScript.

FAQ

What is the difference between var, let, and const?

var is function-scoped and hoisted. let and const are block-scoped. const cannot be reassigned (but objects can mutate). Prefer const by default, use let when reassignment is needed, and avoid var in modern code.

How does the JavaScript event loop work?

The event loop continuously checks the call stack and task queues. Macrotasks (setTimeout, I/O) are processed one per loop iteration. All microtasks (Promise callbacks, queueMicrotask) are processed between macrotasks. UI rendering occurs after microtask processing.

What is the difference between == and ===?

== performs type coercion before comparison (1 == ’1’ is true). === requires both value and type to be identical (1 === ’1’ is false). Always use === unless you explicitly need type coercion.

When should I use promises vs async/await?

Async/await is syntactic sugar over promises, making asynchronous code read synchronously. Prefer async/await for readability. Use promise combinators (Promise.all, Promise.race) when managing multiple concurrent operations.

What are the most common JavaScript memory leaks?

Global variables, forgotten timers/intervals, detached DOM nodes, closures retaining large objects, and event listeners not removed. Use Chrome DevTools Memory panel to identify leaks. WeakMap/WeakSet help with cache-related leaks.

For a comprehensive overview, read our article on Javascript Arrays Guide.

For a comprehensive overview, read our article on Javascript Closures Guide.

Section: JavaScript 1429 words 7 min read Beginner 756 articles in section Report inaccuracy Back to top