Ad – 728Γ—90
πŸš€ Advanced JS

JavaScript Async/Await – Write Async Code Like Sync Code

The async/await syntax, introduced in ES2017, is built entirely on Promises but gives asynchronous code the look and feel of synchronous code. An async function always returns a Promise; await pauses execution inside that function until the awaited Promise settles. The result is code that is dramatically easier to read, debug, and reason about compared to raw .then() chains.

⏱️ 20 min read 🎯 Advanced πŸ“… Updated 2026

The async Function

Prefix any function declaration, expression, or arrow function with async to make it asynchronous. An async function always returns a Promise β€” if you return a plain value, it is automatically wrapped in Promise.resolve().

JavaScript
// All three are equivalent
async function greet(name) {
  return `Hello, ${name}!`;
}

const greetArrow = async (name) => `Hello, ${name}!`;

const obj = {
  async greet(name) { return `Hello, ${name}!`; }
};

// All return Promises
const result = greet('Alice');
console.log(result instanceof Promise);   // true
result.then(msg => console.log(msg));     // Hello, Alice!

// Return value is auto-wrapped
async function add(a, b) {
  return a + b;    // returns Promise.resolve(a + b)
}

add(3, 4).then(sum => console.log(sum));  // 7
β–Ά Output
true
Hello, Alice!
7

The await Expression

await can only be used inside an async function. It pauses the function's execution until the awaited Promise resolves, then returns the resolved value. Other code outside the async function continues to run while it is paused.

JavaScript
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function makeBreakfast() {
  console.log('Starting breakfast...');

  console.log('Boiling water...');
  await delay(500);              // pause 500ms
  console.log('Water boiled!');

  console.log('Toasting bread...');
  await delay(300);
  console.log('Toast ready!');

  console.log('Brewing coffee...');
  await delay(400);
  console.log('Coffee ready!');

  return 'Breakfast is served!';
}

makeBreakfast().then(msg => console.log(msg));
console.log('(Outside async function β€” runs immediately)');
β–Ά Output
Starting breakfast...
Boiling water...
(Outside async function β€” runs immediately)
Water boiled!
Toasting bread...
Toast ready!
Brewing coffee...
Coffee ready!
Breakfast is served!

Error Handling with try/catch/finally

Use standard try/catch with async/await β€” the syntax works exactly as with synchronous code. A rejected Promise becomes a thrown error that the catch block receives.

JavaScript
async function fetchUser(id) {
  if (id <= 0) throw new RangeError('ID must be positive');
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
  if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
  return response.json();
}

async function displayUser(id) {
  let spinner;
  try {
    spinner = '⏳ Loading...';
    console.log(spinner);

    const user = await fetchUser(id);
    console.log(`Name: ${user.name}`);
    console.log(`Email: ${user.email}`);
    return user;
  } catch (err) {
    // Handles BOTH thrown errors and rejected Promises
    console.error(`Error: ${err.message}`);
    return null;
  } finally {
    spinner = null;       // cleanup always runs
    console.log('Loading complete (success or failure)');
  }
}

displayUser(1);   // success path
displayUser(-1);  // error path (throws RangeError)
β–Ά Output
⏳ Loading...
⏳ Loading...
Error: ID must be positive
Loading complete (success or failure)
Name: Leanne Graham
Email: Sincere@april.biz
Loading complete (success or failure)

Sequential vs Parallel await

A common mistake is awaiting Promises sequentially when they are independent β€” this wastes time. Use Promise.all() to run independent async operations in parallel.

JavaScript
function fetchData(label, ms) {
  return new Promise(resolve =>
    setTimeout(() => resolve(`${label} data`), ms)
  );
}

// ❌ Sequential: total ~900ms (each waits for the previous)
async function sequential() {
  const start = Date.now();
  const a = await fetchData('User',    300);
  const b = await fetchData('Posts',   400);
  const c = await fetchData('Friends', 200);
  console.log(`Sequential: ${Date.now() - start}ms`, [a, b, c]);
}

// βœ… Parallel: total ~400ms (the longest one)
async function parallel() {
  const start = Date.now();
  const [a, b, c] = await Promise.all([
    fetchData('User',    300),
    fetchData('Posts',   400),
    fetchData('Friends', 200)
  ]);
  console.log(`Parallel:   ${Date.now() - start}ms`, [a, b, c]);
}

sequential();
parallel();
β–Ά Output
Parallel:   ~400ms  ['User data', 'Posts data', 'Friends data']
Sequential: ~900ms  ['User data', 'Posts data', 'Friends data']
⚠️
Sequential await is not always wrong

Only parallelise operations that are truly independent. If step B requires the result of step A (e.g., get userId then fetch user data), sequential awaits are correct. Forcing parallel execution on dependent operations will cause errors.

async/await vs .then() – Same Power, Better Syntax

Aspect.then() chainsasync/await
ReadabilityCan become nested/longReads like synchronous code
Error handling.catch() at end of chaintry/catch (familiar)
DebuggingStack traces can be confusingClear stack traces
Conditional logicAwkward with if/elseNatural if/else, loops
Return valuesExplicit return in each .then()Use return normally
Under the hoodPromisesPromises (syntactic sugar)

Top-Level await (ES2022 Modules)

In ES modules (type="module" in browsers or .mjs files in Node.js), you can use await at the top level of a file β€” without wrapping in an async function.

JavaScript
// In an ES module (type="module" or .mjs file)
// Top-level await β€” valid without async wrapper

const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const todo = await response.json();
console.log(todo.title);  // "delectus aut autem"

// Useful for dynamic imports
const { default: lodash } = await import('lodash');

// Useful for conditional configuration
const config = process.env.NODE_ENV === 'test'
  ? await import('./config.test.mjs')
  : await import('./config.prod.mjs');
πŸ’‘
Async IIFE for non-module environments

When top-level await is unavailable (CommonJS, older environments), wrap your code in an immediately-invoked async function: (async () => { ... })(). This is a common pattern in Node.js scripts.

Async Class Methods

JavaScript
class UserService {
  #baseUrl = 'https://jsonplaceholder.typicode.com';

  async getUser(id) {
    const res = await fetch(`${this.#baseUrl}/users/${id}`);
    if (!res.ok) throw new Error(`User ${id} not found`);
    return res.json();
  }

  async getPosts(userId) {
    const res = await fetch(`${this.#baseUrl}/posts?userId=${userId}`);
    return res.json();
  }

  async getUserWithPosts(userId) {
    // Parallel fetch β€” both requests fire at the same time
    const [user, posts] = await Promise.all([
      this.getUser(userId),
      this.getPosts(userId)
    ]);
    return { ...user, posts };
  }
}

const svc = new UserService();
svc.getUserWithPosts(1)
  .then(data => {
    console.log(`${data.name} has ${data.posts.length} posts`);
  })
  .catch(console.error);
β–Ά Output
Leanne Graham has 10 posts
Ad – 336Γ—280

πŸ‹οΈ Practical Exercise

Build a DataLoader class that:

  • Has an async load(urls) method that fetches multiple URLs in parallel with Promise.all.
  • Has an async loadSequential(urls) method using a for...of loop.
  • Both methods return an array of parsed JSON objects.
  • Wrap each fetch in try/catch so a single failure doesn't abort the whole batch β€” return null for failed items.
  • Add a private #cache Map to avoid re-fetching the same URL.

πŸ”₯ Challenge Exercise

Implement an asyncQueue function that processes an array of items through an async worker function with a configurable concurrency limit, collects all results in order (even if faster tasks finish first), and reports progress as tasks complete. No external libraries allowed β€” only async/await and Promises.

Interview Questions

  • What does an async function always return?
  • Can you use await outside an async function? (What about top-level await?)
  • How does try/catch work with async/await?
  • What is the performance problem with sequential awaits on independent Promises?
  • How does async/await differ from .then() at the engine level?
  • What is an async IIFE and when would you use one?

πŸ“‹ Summary

  • async functions always return Promises; plain return values are auto-wrapped.
  • await pauses the async function until the Promise resolves, returning its value.
  • Use try/catch/finally for error handling β€” rejected Promises become throw errors.
  • Use Promise.all() to run independent async operations in parallel, not sequential awaits.
  • Top-level await is available in ES modules (ES2022).
  • async/await is syntactic sugar over Promises β€” same power, much more readable.

Frequently Asked Questions

Does await block the entire JavaScript thread? +

No. await only pauses the async function that contains it. The JavaScript event loop is free to run other code while the awaited Promise is pending. This is fundamentally different from blocking synchronous code.

Can I use async/await with forEach? +

Technically yes β€” you can pass an async function to forEach β€” but forEach does not await the returned Promise, so all iterations run concurrently and errors are silently ignored. Use a for...of loop for sequential async iteration, or Promise.all(arr.map(async ...)) for parallel.

How do I handle errors per-await vs globally? +

For global handling, wrap the entire function body in one try/catch. For per-operation handling, either wrap each await in its own try/catch, or use the pattern const [err, data] = await somePromise.then(d => [null, d]).catch(e => [e, null]) β€” a Go-style error-first return.