Skip to main content
Back to module
JavaScript Essentials · Asynchronous JavaScript

Callbacks, promises, async/await

Reading43mLesson 1 of 3 in this module

Understand why some work finishes later, and sequence it clearly with promises and async/await.

Why this matters

Some work takes time: loading data from a server, reading a file, waiting for a timer. If JavaScript stopped and waited for each of these, the whole page would freeze: no scrolling, no clicks, nothing. Instead, JavaScript starts the slow work, carries on, and deals with the result later. This is called asynchronous code. Every app that talks to an API depends on it, so you need to be able to read and write it confidently.

The problem in one example

console.log("1. start");
setTimeout(() => console.log("2. timer done"), 1000);
console.log("3. end");

The output is 1, 3, then 2 a second later. setTimeout does not pause the program; it schedules a function to run later and returns immediately. Any value produced inside that function is not available on the next line.

Callbacks

The oldest pattern is a callback: you pass in a function to be called when the work finishes.

function loadUser(id, callback) {
  setTimeout(() => callback({ id, name: "Ada" }), 100);
}

loadUser(1, (user) => {
  console.log(user.name);  // "Ada", 100 ms later
});

Callbacks work, but when one step depends on another you end up nesting them inside each other ("callback hell"), and handling errors at each level gets messy.

Promises

A promise is an object that stands for a value that will arrive later. It is in one of three states: pending, fulfilled (it has a value) or rejected (it has an error). You attach what should happen next with .then and .catch:

function wait(ms, value) {
  return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}

wait(100, "hello")
  .then((text) => text.toUpperCase())
  .then((upper) => console.log(upper))   // "HELLO"
  .catch((err) => console.error(err));

Each .then returns a new promise, so steps chain top to bottom instead of nesting. One .catch at the end handles a failure at any step. Most browser APIs, including fetch, return promises, so you will mostly use promises rather than create them.

async and await

async/await is a nicer way to write the same thing. Inside an async function, await pauses that function (not the whole page) until a promise settles, and gives you its value:

async function shout() {
  const text = await wait(100, "hello");
  return text.toUpperCase();
}

shout().then((result) => console.log(result));  // "HELLO"

Two rules to remember:

  1. await only works inside an async function (or at the top level of a module).
  2. An async function always returns a promise. Whatever you return becomes the promise's value, so callers either await it or use .then.

Sequencing dependent steps

Suppose fetchUser(id) and fetchPosts(userId) both return promises, and you need the user before you can load their posts:

async function loadProfile(id) {
  const user = await fetchUser(id);        // wait for the user
  const posts = await fetchPosts(user.id); // then their posts
  return { name: user.name, postCount: posts.length };
}

That reads like ordinary step-by-step code, which is the whole point of await.

Running independent work in parallel

If steps do not depend on each other, awaiting them one by one wastes time. Promise.all starts them together and waits for all of them:

async function loadNames(ids) {
  const users = await Promise.all(ids.map((id) => fetchUser(id)));
  return users.map((u) => u.name);
}

Three requests of 100 ms each take about 100 ms in total with Promise.all, instead of 300 ms in sequence. Promise.all keeps the results in the same order as the input, and rejects as soon as any one of them fails.

Errors

A rejected promise inside await throws, so you can use ordinary try/catch:

async function safeLoad(id) {
  try {
    return await fetchUser(id);
  } catch (err) {
    console.error("Could not load user", err);
    return null;
  }
}

The final lesson of this module goes deeper into errors and fallbacks.

Common mistakes

  • Forgetting await. const user = fetchUser(1); gives you a pending promise, so user.name is undefined.
  • Using await in a normal function. It is a syntax error; mark the function async.
  • Expecting an async function to return a plain value. It returns a promise; await it.
  • Awaiting independent work in a loop. Use Promise.all to run it in parallel.
  • No error handling. An unhandled rejection fails silently in the UI; add catch or try/catch.

Quick recap

  • Asynchronous work finishes later; the rest of the code keeps running.
  • Callbacks were first; promises chain with .then/.catch.
  • async/await makes promise code read top to bottom; an async function returns a promise.
  • await for dependent steps, Promise.all for independent ones.

Try it

Open the Do tab: use await to load a profile in order and Promise.all to load several users at once.

Also in this lesson: