Toolmingo
Guides7 min read

How to Use Async/Await in JavaScript: Complete Guide

Master async/await in JavaScript — from Promises to async functions, error handling, parallel execution with Promise.all, and common pitfalls with real-world code examples.

Callbacks led to callback hell. Promises made async code chainable but still verbose. Async/await — introduced in ES2017 — lets you write asynchronous code that reads like synchronous code, without blocking the thread.

This guide covers everything: how async/await works under the hood, error handling, parallel execution, and pitfalls to avoid.


The Problem Async/Await Solves

Before async/await, fetching data looked like this:

// Callback hell
getUserId(function(id) {
  getProfile(id, function(profile) {
    getPosts(profile.userId, function(posts) {
      render(posts);
    });
  });
});

// Promises (better, but still chained)
getUserId()
  .then(id => getProfile(id))
  .then(profile => getPosts(profile.userId))
  .then(posts => render(posts))
  .catch(err => console.error(err));

With async/await:

async function loadUserPosts() {
  const id = await getUserId();
  const profile = await getProfile(id);
  const posts = await getPosts(profile.userId);
  render(posts);
}

Same logic, reads top-to-bottom like synchronous code.


How Async/Await Works

The async keyword

async before a function declaration makes it return a Promise, always.

async function greet() {
  return "hello";
}

greet(); // Promise { 'hello' }
greet().then(console.log); // "hello"

Even if you return a plain value, async wraps it in Promise.resolve().

The await keyword

await pauses execution inside an async function until the Promise resolves. The thread is not blocked — other code runs while waiting.

async function example() {
  console.log("start");
  const result = await fetch("https://api.example.com/data");
  console.log("end"); // runs only after fetch resolves
}

await can only be used inside an async function (or at the top level of an ES module).

Quick reference

Syntax What it does
async function foo() {} Returns a Promise
const x = await promise Waits for promise to resolve, assigns value
await Promise.resolve(42) Immediately resolves to 42
await new Promise(r => setTimeout(r, 1000)) Waits 1 second
Top-level await Supported in ES modules (.mjs or type: "module")

Error Handling

try/catch

The most readable way — mirrors synchronous error handling:

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

.catch() on the call

If you don't want to wrap every call in try/catch:

const user = await fetchUser(1).catch(err => {
  console.error(err);
  return null;
});

await with a fallback

const data = await riskyOperation().catch(() => defaultValue);

Error handling patterns comparison

Pattern When to use
try/catch inside async fn Most common, cleanest for sequential logic
.catch() on caller When you want a fallback without try/catch boilerplate
Promise.allSettled When you want all results, including failures
Custom to() helper When you prefer Go-style [err, result] tuples

Real-World Example: Fetch API

async function getWeather(city) {
  const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&appid=YOUR_KEY`;

  try {
    const response = await fetch(url);

    if (!response.ok) {
      throw new Error(`Weather API error: ${response.status}`);
    }

    const data = await response.json();
    return {
      city: data.name,
      temp: Math.round(data.main.temp - 273.15), // Kelvin → Celsius
      description: data.weather[0].description,
    };
  } catch (err) {
    if (err.name === "TypeError") {
      throw new Error("Network error — check your connection");
    }
    throw err;
  }
}

// Usage
const weather = await getWeather("London");
console.log(`${weather.city}: ${weather.temp}°C, ${weather.description}`);

Parallel Execution

The sequential trap

This is slower than it needs to be:

// BAD: runs sequentially — total wait = 3s if each takes 1s
const users = await getUsers();    // wait 1s
const posts = await getPosts();    // wait 1s
const comments = await getComments(); // wait 1s

Promise.all — run in parallel

// GOOD: all three fire simultaneously — total wait ≈ 1s
const [users, posts, comments] = await Promise.all([
  getUsers(),
  getPosts(),
  getComments(),
]);

Promise.all rejects immediately if any promise rejects. Use it when you need all results and a single failure should abort.

Promise.allSettled — get all results regardless

const results = await Promise.allSettled([
  fetchProfile(userId),
  fetchPosts(userId),
  fetchFollowers(userId),
]);

for (const result of results) {
  if (result.status === "fulfilled") {
    console.log("Got:", result.value);
  } else {
    console.error("Failed:", result.reason);
  }
}

Promise.race — first one wins

// Timeout pattern: reject if fetch takes more than 5s
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);

try {
  const response = await fetch(url, { signal: controller.signal });
  clearTimeout(timeoutId);
  return await response.json();
} catch (err) {
  if (err.name === "AbortError") throw new Error("Request timed out");
  throw err;
}

Promise.any — first success wins

// Try multiple CDN mirrors, use whichever responds first
const data = await Promise.any([
  fetch("https://cdn1.example.com/data.json"),
  fetch("https://cdn2.example.com/data.json"),
  fetch("https://cdn3.example.com/data.json"),
]).then(r => r.json());

Async Loops

Sequential loop (each waits for previous)

const userIds = [1, 2, 3, 4, 5];

for (const id of userIds) {
  const user = await getUser(id); // waits before next iteration
  console.log(user.name);
}

Parallel loop (all at once)

// Fire all requests simultaneously
const users = await Promise.all(userIds.map(id => getUser(id)));

Batched parallel (n at a time)

async function batchProcess(items, batchSize, fn) {
  const results = [];
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    const batchResults = await Promise.all(batch.map(fn));
    results.push(...batchResults);
  }
  return results;
}

// Process 100 users, 10 at a time
const users = await batchProcess(userIds, 10, getUser);

forEach doesn't work with await

// BUG: forEach doesn't await — all run and you get no results
items.forEach(async (item) => {
  await processItem(item); // this is ignored by forEach
});

// FIX: use for...of
for (const item of items) {
  await processItem(item);
}

Async/Await in Different Contexts

Node.js (CommonJS)

const https = require("https");
const { promisify } = require("util");

// Promisify callback-based APIs
const readFile = promisify(require("fs").readFile);

async function readConfig() {
  const content = await readFile("config.json", "utf8");
  return JSON.parse(content);
}

// Top-level: wrap in IIFE or use async main
(async () => {
  const config = await readConfig();
  console.log(config);
})();

Node.js (ES modules)

// package.json: "type": "module"  OR filename .mjs
import { readFile } from "fs/promises";

// Top-level await works here
const config = JSON.parse(await readFile("config.json", "utf8"));

React (useEffect)

// Can't make useEffect itself async — use inner function
useEffect(() => {
  let cancelled = false;

  async function loadData() {
    try {
      const data = await fetchData(id);
      if (!cancelled) setData(data);
    } catch (err) {
      if (!cancelled) setError(err.message);
    }
  }

  loadData();
  return () => { cancelled = true; }; // cleanup
}, [id]);

TypeScript

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json() as Promise<User>;
}

Common Mistakes

Mistake Problem Fix
await outside async function SyntaxError Use async on enclosing function, or top-level await in ESM
Sequential await when parallel is possible Slow — waits unnecessarily Use Promise.all for independent calls
forEach with await await is ignored Use for...of or Promise.all(arr.map(...))
Not checking response.ok after fetch Silent failures with 4xx/5xx Always check response.ok before .json()
Swallowing errors in catch Hides bugs Log and rethrow, or return a typed error
Missing await on a Promise Gets Promise object instead of value Add await; TypeScript catches this with strict mode
Unhandled rejected Promise Node.js crash / silent browser failure Always add .catch() or try/catch

Async/Await vs Promises vs Callbacks

Feature Callbacks Promises Async/Await
Readability Poor (nesting) OK (chaining) Best (linear)
Error handling Manual .catch() try/catch
Debugging (stack traces) Poor Moderate Good
Parallel execution Complex Promise.all await Promise.all
Available since ES5 ES6 (2015) ES2017
Top-level usage Yes Yes ESM only

Async/await is Promises — it compiles to Promise chains. You can mix both freely.


FAQ

Can I use await at the top level?
Yes, in ES modules (files with .mjs extension, or "type": "module" in package.json). In CommonJS you still need to wrap in an async IIFE.

Does await block the event loop?
No. await suspends only the current async function. The event loop continues to process other tasks while waiting.

What happens if I forget await?
The expression evaluates to a Promise object instead of the resolved value. TypeScript with strictNullChecks and @typescript-eslint/no-floating-promises catches this automatically.

How do I set a timeout on an await?
Use AbortController with fetch, or Promise.race with a rejection timeout: await Promise.race([myPromise, new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 5000))]).

Can async functions return multiple values?
Return an object or array: return { user, posts } then const { user, posts } = await loadAll().

Is async/await supported everywhere?
Yes — all modern browsers (Chrome 55+, Firefox 52+, Safari 10.1+, Edge 15+) and Node.js 7.6+. For older environments, Babel transpiles it to generator functions.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools