Learn / Programming / JavaScript / Promises and async/await

Intermediate 16 min

Promises and async/await

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

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.

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);
fetch gotcha

fetch only rejects on network failure. A 404 or 500 still resolves, so always check res.ok.

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.

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.

Try it yourself

Write retry(fn, times), an async function that calls fn and, if it throws, tries again up to times attempts before giving up.

Show solution
async function retry(fn, times) {
  let lastErr;
  for (let i = 0; i < times; i++) {
    try {
      return await fn();
    } catch (err) {
      lastErr = err;
    }
  }
  throw lastErr;
}