Chapter 21 of 28 · JavaScript for Beginners — Complete Free Tutorial

Async/Await

async/await (ES2017) is syntactic sugar over Promises that lets asynchronous code read like synchronous code — top to bottom, with ordinary try/catch. Once you use it, callback chains feel like the past. This chapter covers the syntax, error handling, sequencing vs parallelising, and the patterns that matter in real apps.

The basics

async function greet() {
  return 'Hello';            // async functions ALWAYS return a promise
}
greet().then((v) => console.log(v));   // → 'Hello'

async function showName() {
  const name = await Promise.resolve('Asha');   // unwrap the promise
  console.log(name);                           // → Asha
}

Two rules:

  1. async — the function always returns a promise (even for plain values).
  2. await — pauses that function only until the promise settles, then yields its value. The main thread stays free.

await with real-world APIs

async function loadUser(id) {
  const res = await fetch(`/api/users/${id}`);
  const user = await res.json();
  console.log(user.name);
}

fetch returns a promise; res.json() also returns a promise — each gets its own await. (The .json() await is easy to forget: skipping it logs an unresolved promise.)

try/catch/finally — the same as sync code

async function loadUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    console.error('load failed:', err);
    return null;                    // graceful fallback
  } finally {
    hideSpinner();
  }
}

Expected output: network errors, HTTP errors and JSON parse errors all land in the same catch — no .catch() plumbing, no nested handlers. This is why async/await won.

Parallel vs sequential — the critical distinction

// ❌ sequential — 3 seconds for three 1s calls
const a = await getA();
const b = await getB();
const c = await getC();

// ✅ parallel — one second total
const [a, b, c] = await Promise.all([getA(), getB(), getC()]);

await pauses, so calls placed one after another run in series. Independent requests must be started together, then awaited together (or use Promise.allSettled for partial tolerance — chapter 20).

// mixed: parallel where independent, sequential where dependent
const [profile, theme] = await Promise.all([fetchProfile(), fetchTheme()]);
const orders = await fetchOrders(profile.id);   // needs profile.id

Looping with await

for (const id of ids) {                 // ✅ sequential by design
  await save(id);
}

for (const id of ids.map(save)) {}      // wrong shape
await Promise.all(ids.map(save));       // ✅ all at once

for await (const chunk of stream) {     // async iteration (streams, APIs)
  process(chunk);
}

forEach does not work with await (it ignores the returned promises) — use for…of with await, or Promise.all over map.

Arrow and object-method forms

const fetchAll = async () => {
  const res = await fetch('/api');
  return res.json();
};

const api = {
  async get(path) {
    const res = await fetch(path);
    if (!res.ok) throw new Error('failed');
    return res.json();
  },
};

const data = await api.get('/users');   // top-level await: works in modules (ch. 22)

Common patterns

Timeout wrapper

async function withTimeout(promise, ms = 5000) {
  const timeout = new Promise((_, rej) =>
    setTimeout(() => rej(new Error(`timed out after ${ms}ms`)), ms));
  return Promise.race([promise, timeout]);
}

Retry with backoff

async function retry(fn, attempts = 3) {
  let lastErr;
  for (let i = 0; i < attempts; i++) {
    try { return await fn(); }
    catch (err) {
      lastErr = err;
      await new Promise((r) => setTimeout(r, 2 ** i * 300));   // 300, 600, 1200
    }
  }
  throw lastErr;
}

Safe fetch helper

async function getJSON(url, options) {
  const res = await fetch(url, options);
  if (!res.ok) {
    const err = new Error(`HTTP ${res.status}`);
    err.status = res.status;
    throw err;
  }
  return res.json();
}

Error handling rules

  • Every await that can fail sits inside try/catch — or the caller must consume the rejected promise.
  • Check res.ok — a 404/500 is not a rejected promise (fetch only rejects on network failure). This trips up everyone once.
  • Do not await inside loops accidentally — decide explicitly: parallel or sequential?

Common mistakes

  • await outside an async function — syntax error (except top-level await in modules).
  • Forgetting await on .json() — you get a promise object instead of data.
  • Sequential awaits for independent calls — multiplies load time needlessly.
  • forEach + await — does not wait; use for…of or Promise.all(map).
  • Awaiting non-promises — harmless but noisy; await only where async actually happens.
  • Swallowing in catch — return a fallback, rethrow, or surface a UI error state.

Practice checklist

  1. Convert a promise chain from chapter 20 into async/await with try/catch.
  2. Time sequential vs Promise.all versions of three fake 1-second tasks — confirm the difference.
  3. Write retry() and prove it re-runs a failing fake function three times before throwing.
  4. Build a getUserWithPosts(id) that runs one dependent and one independent request with the right mix of parallelism.

Time to use these skills on real networks — next: Fetch API & Working with APIs.

Quiz: Async/Await

5 questions · 70% to pass

Frequently Asked Questions

JavaScript is the programming language of the web. It adds behaviour to pages: responding to clicks and input, validating forms, fetching data from APIs, updating the DOM without reloading, and much more. It also runs on servers (Node.js), in desktop and mobile apps, and in build tooling.

They are unrelated languages that share a similar name for historical marketing reasons. Java is a statically-typed compiled language; JavaScript is a dynamically-typed interpreted language designed for the browser. Knowing one does not imply knowledge of the other.

Helpful but not mandatory — HTML gives you the structure JavaScript manipulates and CSS the styling it toggles. If you know basic tags, classes and selectors, you have enough to start; the DOM chapters assume only that much.

Use const by default and let when the value must change. Avoid var entirely in modern code: it is function-scoped instead of block-scoped, hoists differently, and can create accidental globals. This course uses const and let throughout.

The Document Object Model is the browser’s in-memory tree of objects representing your HTML. JavaScript reads and modifies that tree — selecting elements, changing text, adding nodes, attaching event listeners — and the browser re-renders the page to match.

== compares after converting types (so 5 == "5" is true), while === requires the same value AND the same type (5 === "5" is false). Loose equality has famously confusing rules — use === and !== everywhere unless you have a specific reason not to.

JavaScript is single-threaded, so long-running work (timers, fetches, events) is handled by the browser and its callbacks are queued. The event loop moves queued callbacks onto the call stack only when it is empty, with promise callbacks (microtasks) taking priority over timers. It is why asynchronous code never freezes the page.

A Promise is an object representing a future value — pending, fulfilled or rejected — that can be chained and combined (Promise.all). async/await is syntax sugar over promises that lets asynchronous code read top-to-bottom with ordinary try/catch error handling.

Yes. Frameworks are built on exactly these fundamentals — components are functions, state is your variable discipline, effects are events, rendering is DOM work. Developers who learn vanilla first understand what the framework is doing and debug far faster.

Type out the examples instead of copying, build small end-to-end projects (expense tracker, weather app, quiz), handle every UI state (loading, empty, success, error), and revisit fundamentals while reading real code. Consistency beats volume — a little most days outpaces occasional marathons.