Skip to content

Async JavaScript

Callbacks, promises, async/await, and fetching data from APIs without blocking.

What you'll learn

  • Why JavaScript is single-threaded and what the event loop does
  • The problem with callbacks (callback hell)
  • Promises: then, catch, finally, and chaining
  • async/await for linear, readable async code
  • Fetching JSON from an API with fetch()

Concept

The Event Loop

JavaScript is single-threaded — it runs one task at a time. To avoid freezing the page on slow operations, the event loop pushes long-running work (network requests, timers) to the browser's background APIs and runs the callback when the result is ready.

This means async code does not run top-to-bottom. Understanding this is the key to avoiding race conditions and bugs.

Callbacks (the old way)

A callback is a function passed to be called later:

setTimeout(() => {
  console.log("runs after 1 second");
}, 1000);

Nesting many callbacks creates callback hell — deeply indented, hard-to-read code:

getUser(id, (user) => {
  getOrders(user, (orders) => {
    getOrderItems(orders[0], (items) => {
      // ... keeps nesting
    });
  });
});

Promises

A promise represents a value that will be available in the future. It can be pending, fulfilled, or rejected.

fetch("https://api.example.com/data")
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error("Failed:", error))
  .finally(() => console.log("Done"));

Promises chain flatly and propagate errors to the next catch, which is much cleaner than nested callbacks.

async / await

async/await (ES2017) lets you write async code that looks synchronous. An async function always returns a promise, and await pauses until the promise resolves:

async function loadData() {
  try {
    const response = await fetch("https://api.example.com/data");
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("Failed:", error);
  }
}

This is the modern standard. Use it instead of then chains whenever possible.

Fetching Data

fetch() is the built-in API for making HTTP requests. It returns a promise that resolves to a Response object:

const res = await fetch("/api/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Ada" }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const users = await res.json();

Always check res.okfetch only rejects on network errors, not on HTTP 404 or 500.

Running Promises in Parallel

// Sequential — slow
const a = await fetchA();
const b = await fetchB();

// Parallel — faster
const [aResult, bResult] = await Promise.all([fetchA(), fetchB()]);

Promise.all runs promises concurrently and waits for all of them. Use it to speed up independent requests.

Example

              // async/await with fetch and error handling
async function getUser(userId) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    const user = await response.json();
    return user;
  } catch (error) {
    console.error("Failed to fetch user:", error.message);
    return null;
  }
}

// Run multiple requests in parallel
async function loadDashboard() {
  const [user1, user2, user3] = await Promise.all([
    getUser(1),
    getUser(2),
    getUser(3),
  ]);

  const users = [user1, user2, user3].filter(Boolean);
  users.forEach((u) => console.log(`${u.name} — ${u.email}`));
}

// A simple promise you can create yourself
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function countdown() {
  for (let i = 3; i > 0; i--) {
    console.log(i);
    await delay(1000);
  }
  console.log("Go!");
}

loadDashboard();
countdown();
            

Shows a real fetch call with error handling, Promise.all for parallel requests, creating a promise from scratch (delay), and using await with a timer to build a countdown.

Try it

Common mistakes

The mistake

Forgetting await and getting a Promise object instead of the value

The fix

await pauses until the promise resolves. Without it, you get the Promise object itself. Always use await inside async functions when you need the resolved value.

The mistake

Not checking response.ok after fetch

The fix

fetch only rejects on network errors. HTTP 404 or 500 still resolve. Always check if (!response.ok) throw new Error(...) before calling response.json().

Related Tools

Related Cheatsheets

Practice Async JavaScript

2 exercises

Practice Async JavaScript

Look up unfamiliar terms

A beginner-friendly glossary explains jargon in plain English — variables, functions, DOM, Promise, and more.

View glossary

Build real projects

Apply what you learned by building guided projects with starter code and solutions.

View projects

Decode error messages

Plain-English explanations of common errors — what they mean, why they happen, and how to fix them.

View errors