Learn / Programming / JavaScript / Promises and async/await

JavaScript · Lesson 5 of 6

Promises and async/await

Handle work that finishes later: timers, fetch and error handling.

  • Intermediate
  • 16 min read
  • 3 objectives

Before this lessonLesson 4: Arrays and Objects

What you will learn

  • Explain the event loop
  • Use async/await
  • Run requests in parallel

JavaScript runs your code on a single thread. To stay responsive while waiting for things like network requests or timers, it hands slow work to the environment and continues. When the result is ready, a callback is queued and runs once the current code has finished. This mechanism is the event loop.

Why waiting is hard

Some work is slow: loading data from a server, reading a file, waiting for a timer. JavaScript has a single main thread, so if it simply stood still until each slow task finished, the whole page would freeze. Instead it starts the slow job, carries on with other work, and comes back when the result is ready. Think of ordering coffee: you give your order, take a buzzer, sit down, and collect the drink when the buzzer goes off. The buzzer is a Promise.

console.log("1: order coffee");
setTimeout(() => console.log("3: coffee is ready"), 100);
console.log("2: check phone while waiting");
Output
1: order coffee
2: check phone while waiting
3: coffee is ready

Notice the order. The timer's message prints last, even though it was written second. The code did not wait; it scheduled the callback and moved on.

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

Even with a delay of 0, the timer callback runs after the synchronous code finishes.

Promises

A promise stands for a value that will arrive later. It is pending, then either fulfilled with a value or rejected with an error. Chain .then() for success and .catch() for failure.

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

wait(100)
  .then(() => console.log("done waiting"))
  .catch((err) => console.error(err));

async / await

async functions always return a promise, and await pauses the function until a promise settles, so asynchronous code reads top to bottom. Wrap awaits in try/catch to handle errors.

async function loadUser(id) {
  try {
    const res = await fetch(`https://api.example.com/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    console.error("failed:", err.message);
    return null;
  }
}
loadUser(1).then(console.log);

Parallel work

Awaiting things one after another makes them run in sequence. When the calls are independent, start them together with Promise.all, which resolves when all are done and rejects if any fails.

const wait = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));

async function main() {
  console.time("parallel");
  const [a, b] = await Promise.all([wait(200, "A"), wait(200, "B")]);
  console.timeEnd("parallel");   // ~200ms, not 400ms
  console.log(a, b);
}
main();
Output
parallel: 201ms
A B

Promise.allSettled waits for every promise and reports each outcome, which is handy when partial failure is acceptable.

A promise has three states

  • pending: the work has started and is not finished.
  • fulfilled: it finished and produced a value.
  • rejected: it failed and produced an error.
const wait = (ms, value) => new Promise(resolve => setTimeout(() => resolve(value), ms));

wait(50, "done").then(v => console.log("resolved with", v));
console.log("this prints first");
Output
this prints first
resolved with done

Same thing, easier to read: async and await

await pauses that function until the promise settles, then gives you the value as if the code were ordinary top-to-bottom. It can only be used inside an async function. Other code keeps running while you wait.

const wait = (ms, value) => new Promise(r => setTimeout(() => r(value), ms));

async function main() {
  console.log("start");
  const a = await wait(50, "first");
  const b = await wait(50, "second");
  console.log(a, b);
  console.log("end");
}
main();
Output
start
first second
end

Handling failure

const failing = () => new Promise((_, reject) => setTimeout(() => reject(new Error("server down")), 20));

async function load() {
  try {
    await failing();
  } catch (err) {
    console.log("caught:", err.message);
  } finally {
    console.log("cleanup");
  }
}
load();
Output
caught: server down
cleanup

Sequential versus parallel

Two awaits in a row run one after the other, which is slow if the tasks are independent. Start them together and wait for both with Promise.all.

const wait = (ms, v) => new Promise(r => setTimeout(() => r(v), ms));

async function main() {
  let t = Date.now();
  await wait(100); await wait(100);
  console.log("one by one:", Math.round((Date.now() - t) / 100) * 100, "ms");

  t = Date.now();
  const [a, b] = await Promise.all([wait(100, "x"), wait(100, "y")]);
  console.log("together:", Math.round((Date.now() - t) / 100) * 100, "ms", a, b);
}
main();
Output
one by one: 200 ms
together: 100 ms x y

A realistic fetch example

async function getUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`);
  if (!response.ok) {
    throw new Error(`Request failed: ${response.status}`);
  }
  return response.json();   // also returns a promise
}

getUser(1)
  .then(user => console.log(user.name))
  .catch(err => console.error(err.message));

Common mistakes

  • Forgetting await, so you log a pending promise instead of its value.
  • Using await inside forEach; it does not wait. Use a for...of loop or Promise.all with map.
  • Not handling rejections, which produce "unhandled promise rejection" errors.

Key takeaways

  • Slow work returns a Promise (pending, fulfilled or rejected) so the page never freezes.
  • async/await lets you read asynchronous code top to bottom.
  • Wrap awaits in try/catch; check response.ok when using fetch.
  • Use Promise.all for independent tasks that can run together.
// Write your solution here
Up next · Lesson 6The DOM and EventsSelect elements, react to clicks and update the page.