Skip to content

Intermediate · Async JavaScript, modules and tools · topic 40 of 64 · 35 min

Promises and the event loop

Explanation

JavaScript in a page does one thing at a time, so slow jobs like downloads and timers are asynchronous: you start them now and get the result later. A promise is an object that stands for that future result. It is pending, then fulfilled or rejected, and you react with then, catch and finally, or wait for many at once with Promise.all.

JavaScript does one thing at a time

JavaScript in a web page runs on a single thread. A thread is one line of work: the browser runs your code one statement at a time, never two at once. Most code is fast, so this is fine.

Some jobs are slow: downloading data from a server, waiting for a timer, waiting for the user to click. If JavaScript stood still until these finished, the page would freeze: no scrolling, no typing. So these jobs are asynchronous (async): you start them, your code carries on, and you get the result later.

Think of it like this: Think of a busy food court. You pay at the counter and get a token number. You do not stand at the counter blocking the line; you sit down and chat. The counter serves the next person. When your food is ready, your number flashes on the screen and you collect it. The token is a promise: a ticket for a result that will come later.

The event loop in simple words

  • Call stack: the code running right now. Only one piece at a time.
  • Browser helpers (Web APIs): the browser does the waiting for timers and network requests, outside your code.
  • Task queue: finished jobs such as a timer callback or a click handler wait here in a line.
  • Microtask queue: promise callbacks (then, catch, finally, code after await) wait here. They are special: they all run as soon as the stack is empty, before the next task.
  • Event loop: a simple loop. When the call stack is empty, run all microtasks, then take the next task from the queue, and repeat.
javascript
console.log('A');
setTimeout(() => console.log('B'), 0); // a task
Promise.resolve().then(() => console.log('C')); // a microtask
console.log('D');
// Output: A, D, C, B
Normal code first, then promise callbacks, then timers, even with a 0 ms timer

Callbacks and callback hell

You met callbacks in beginner JavaScript: a function you pass to another function, to be called later. Before promises, every async job took a callback. When one step needs the result of the step before, the callbacks nest deeper and deeper.

javascript
getUser(userId, (user) => {
  getOrders(user.id, (orders) => {
    getOrderItems(orders[0].id, (items) => {
      showItems(items);
      // and errors? each level needs its own error check...
    });
  });
});
"Callback hell": code that drifts to the right and is hard to read and fix

Promises: a value that comes later

StateMeaningWhat runs
pendingStill waiting; no result yetNothing yet
fulfilledFinished with a value (resolved)then callbacks
rejectedFailed with a reason, usually an Errorcatch callbacks

A promise starts pending and settles only once: it becomes fulfilled or rejected, and then it never changes again. finally runs in both cases.

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

wait(1000).then((ms) => console.log(`Waited ${ms} ms`));
Making your own promise: call resolve(value) when the job is done, or reject(error) when it fails
javascript
showSpinner();
getUserPromise(42)
  .then((user) => getOrdersPromise(user.id)) // return the next promise
  .then((orders) => showOrders(orders))
  .catch((error) => showError(error.message)) // any step that fails lands here
  .finally(() => hideSpinner());             // runs on success and on failure
Chaining: each then returns a new promise, so the steps read top to bottom

Waiting for many promises

MethodSettles whenUse it when
Promise.all(list)All fulfil (gives an array of values), or the first one rejectsYou need every result, e.g. user + cart + offers for one page
Promise.allSettled(list)All have settled; gives { status, value or reason } for eachYou want every result even if some fail, e.g. 5 shops' prices
Promise.race(list)The first one settles, success or failureA timeout: race the request against a timer
Promise.any(list)The first one fulfils; rejects only if all fail (AggregateError)Several mirrors of the same data; take the fastest that works
javascript
const [user, cart] = await Promise.all([
  getUserPromise(42),
  getCartPromise(42),
]);
console.log(user.name, cart.items.length);
Promise.all starts both jobs together and gives the results in the same order
Tip: Inside a then, always return the next promise. If you forget the return, the chain does not wait for it, and errors from it skip your catch.
Careful: Always handle errors: end a chain with catch (or use try/catch with await). A rejected promise nobody handles shows "Uncaught (in promise)" in the Console. Also remember: promises do not make heavy work faster. A long loop still freezes the page, because it still runs on the one thread.

Watch the lesson

Promises and the event loop · 1/7

One thread Many slow jobs

JavaScript in a page does one thing at a time. So how does it wait for a server without freezing?

Prefer a video by a teacher? Find free videos on “javascript promises event loop explained for beginners” on YouTube ↗

In real life: A train search page

On a railway search page, you pick two stations and press Search. The page shows a spinner, starts three requests together with Promise.all (trains, fares and seat availability) and stays smooth while it waits. When all three arrive, it draws the list; if any one fails, catch shows "Could not load trains, please try again", and finally hides the spinner in both cases.

Practice (0/3 done)

Task 1 · Code · Javascript

Write a function wait(ms) that returns a Promise. The promise must resolve after ms milliseconds (use setTimeout), with the number ms as its value. Example: await wait(40) waits about 40 ms and gives 40.

Task 2 · Code · Javascript

You ask several shops for the price of the same item. Some shops fail. Write async function summarize(promises) that waits for ALL of them with Promise.allSettled and returns { ok, failed }: ok is the list of values of the fulfilled promises, failed is the list of error messages (reason.message) of the rejected ones, both in the original order. Example: [price 120, error "Shop closed", price 80] gives { ok: [120, 80], failed: ["Shop closed"] }.

Task 3 · Practice

Predict, then check. On paper, write the order in which these lines print: console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); wait(10).then(() => console.log(4)); console.log(5). (wait is your helper from task 1.) Then paste the code into the DevTools Console and compare. Explain each line in one sentence using the words call stack, microtask and task.

Quick quiz

Question 1

1.Which of these is NOT a state of a promise?

Question 2

2.What does this print? console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C')); console.log('D');

Question 3

3.You use Promise.all with three requests and the second one fails. What happens?

Question 4

4.A price comparison page asks 5 shops and must show every price that came back, even if some shops fail. Which method fits?

Question 5

5.Where should you hide a loading spinner so it hides after both success and failure?

Read more (free)

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

← Previous: Saving data in the browser