Courses Learn Blog Contact
Get Started →
Lesson 7

Day 7: Asynchronous JavaScript

The event loop, callbacks, Promises and async/await, with real timings showing why Promise.all halves the wait, and the three states every asynchronous interface needs.

Webbo3 Level 2: JavaScript Development 20 min read Free

Webbo3 Level 2 · JavaScript Development · Day 7 of 20

Asynchronous JavaScript

What your code does while it is waiting for something slow, and why the answer changes the way you write it.

By the end of today you can

  • Explain why JavaScript does not simply stop and wait
  • Read the order things actually happen in, rather than the order they are written
  • Write and consume a Promise
  • Use async and await to make asynchronous code read like ordinary code
  • Handle a failure with try/catch instead of letting the page die quietly

Everything you have written so far finished instantly. From tomorrow you will ask a server in another country for data, and that takes somewhere between a tenth of a second and forever. JavaScript has one thread: if it stopped and waited, the whole page would freeze, no button would respond and no animation would move. So it does not wait. Understanding what it does instead is today.

1. Order is not the order you wrote

JSapp.js
console.log('1. first');

setTimeout(() => {
  console.log('3. inside the timer, zero milliseconds');
}, 0);

console.log('2. last line of the file');
Console

Zero milliseconds, and it still runs last. setTimeout does not mean "run this in 0ms", it means "hand this to the browser and carry on". The browser puts the function in a queue, JavaScript finishes everything it was already doing, and only when the call stack is completely empty does it pick the next thing off the queue.

The mental model worth keeping

JavaScript runs one thing at a time, to completion, and never interrupts itself. Anything slow is handed out to the browser, which brings the result back later by putting a function in a queue. That is the whole of the event loop, and it explains every surprising order you will ever see.

2. The bug this creates

JSapp.js
function getUser() {
  let user;
  setTimeout(() => { user = { name: 'Chidinma' }; }, 50);
  return user;                 // runs immediately, long before 50ms
}

console.log(getUser());
Console

undefined, every time. The return runs immediately; the assignment happens 50 milliseconds later, into a variable nobody is looking at any more. You cannot return a value that has not arrived yet. Every asynchronous tool in the language exists to answer this one problem.

3. Callbacks, and why they stopped being enough

JSapp.js
function getUser(callback) {
  setTimeout(() => callback({ name: 'Chidinma' }), 50);
}

getUser((user) => {
  console.log('got', user.name);
});
Console

That works. The trouble starts when one thing depends on the next, and then on the next again:

JSapp.js
getUser(1, (user) => {
  getOrders(user.id, (orders) => {
    getItems(orders[0].id, (items) => {
      getPrice(items[0].id, (price) => {
        console.log(price);            // four levels deep, and no error handling yet
      });
    });
  });
});

That shape has a name, callback hell, and the reason it is more than ugly is error handling: every one of those four steps can fail, and each needs its own check. Promises were added to the language in 2015 specifically to flatten this.

4. Promises

A Promise is an object representing a value that has not arrived yet. It is in one of three states: pending, then either fulfilled with a value or rejected with a reason. It never goes back.

JSapp.js
function getUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id <= 0) {
        reject(new Error('No such user'));
        return;
      }
      resolve({ id, name: 'Chidinma' });
    }, 50);
  });
}

getUser(1)
  .then((user) => console.log('resolved:', user.name))
  .catch((error) => console.log('rejected:', error.message));

getUser(-1)
  .then((user) => console.log('resolved:', user.name))
  .catch((error) => console.log('rejected:', error.message));
Console

.then() runs on success, .catch() runs on failure, and because .then() returns a Promise you can chain them into a flat line rather than a pyramid. One .catch() at the end catches a failure from any step above it, which is the real win.

5. async and await

Promises flattened the pyramid. async/await removes it entirely and lets asynchronous code read top to bottom like everything else you have written.

JSapp.js
function delay(ms, value) {
  return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}

async function loadDashboard() {
  console.log('loading...');
  const user = await delay(40, { name: 'Chidinma' });
  const orders = await delay(40, ['order-1', 'order-2']);
  return `${user.name} has ${orders.length} orders`;
}

loadDashboard().then((summary) => console.log(summary));
Console
  • await may only be used inside a function marked async.
  • await pauses that function until the Promise settles. It does not freeze the page: everything else carries on.
  • An async function always returns a Promise, even when you return a plain value. That is why the call above still needs .then().

The mistake that prints a Promise instead of your value

Calling an async function without await or .then() hands you the Promise object rather than the value inside it. Node prints it as Promise { 4500 } and a browser as Promise {<fulfilled>: 4500}; while it is still running, both say pending. Any time the word Promise appears in your output where a value should be, you forgot to await.

JSapp.js
async function getTotal() { return 4500; }

console.log(getTotal());          // the Promise, not the number
getTotal().then((n) => console.log('awaited:', n));
Console

6. Two awaits in a row is often a mistake

await is sequential. If the second request does not need the first one's answer, waiting for them one after the other doubles the time for no reason.

JSapp.js
function delay(ms, value) {
  return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}

async function sequential() {
  const started = Date.now();
  const a = await delay(60, 'courses');
  const b = await delay(60, 'students');
  return `sequential: ${a} + ${b} in about ${Math.round((Date.now() - started) / 10) * 10}ms`;
}

async function parallel() {
  const started = Date.now();
  const [a, b] = await Promise.all([delay(60, 'courses'), delay(60, 'students')]);
  return `parallel:   ${a} + ${b} in about ${Math.round((Date.now() - started) / 10) * 10}ms`;
}

sequential().then(console.log).then(() => parallel().then(console.log));
Console

Same two pieces of work, roughly half the wait. Use Promise.all whenever the requests are independent. Use sequential await only when the second genuinely needs something from the first.

7. Handling failure

JSapp.js
function mightFail(shouldFail) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) reject(new Error('Network unreachable'));
      else resolve('data arrived');
    }, 20);
  });
}

async function load(shouldFail) {
  try {
    const result = await mightFail(shouldFail);
    console.log('ok:', result);
  } catch (error) {
    console.log('handled:', error.message);
  } finally {
    console.log('finally always runs, spinner off');
  }
}

load(false).then(() => load(true));
Console

An unhandled rejection is a silent page

Forget the try/catch and a failed request throws into nothing. The user sees a spinner that never stops, no error, no explanation, and they conclude your app is broken rather than their connection. finally is where the spinner gets turned off, precisely because it runs whether things worked or not.

8. The three states a real interface must have

This is the part beginners skip and users notice. Anything asynchronous has three possible states on screen, and you have to design all three.

StateWhat the user seesWhat you must do
LoadingA spinner, a skeleton, or "Loading..."Show it before the request, hide it in finally
ErrorA plain message and a way to retryCatch, and say what happened in words a human understands
Empty"No results for that search"Success with zero items is not an error, and it is not blank either
JSstates.js
// the shape every fetch in this course will follow
async function load() {
  showLoading();
  try {
    const items = await getItems();
    if (items.length === 0) showEmpty();
    else showItems(items);
  } catch (error) {
    showError(error.message);
  } finally {
    hideLoading();
  }
}
Result in the browser

Do this now

  1. Write delay(ms) returning a Promise, then an async function that logs "one", waits a second, logs "two", waits again, logs "three". Watch the timing.
  2. Rewrite it with Promise.all so all three finish together, and time both.
  3. Write a function that rejects half the time at random, call it ten times, and count successes and failures with try/catch.
  4. Predict the output of the ordering example at the top before running it, then add a Promise.resolve().then() into the mix and predict again.

Checkpoint

Q1. Why does `setTimeout(fn, 0)` still run after the last line of the file?

Because it is queued rather than run. JavaScript finishes all the work it already has before taking anything off the queue, so "0 milliseconds" means "as soon as I am completely free", not "now".

Q2. Your console prints `Promise { }`. What did you forget?

To await it, or to attach .then(). You printed the Promise object itself rather than the value it will eventually hold.

Q3. When should you use `Promise.all` instead of two awaits?

Whenever the second request does not need the first one's result. Two independent awaits take the sum of both waits; Promise.all takes the longer of the two.

Q4. Why does the spinner belong in `finally` rather than after the `try`?

Because finally runs whether the request succeeded or threw. Put it after the try block only and a failed request leaves the spinner turning forever.

Tick before you move on

  • I predicted the ordering example correctly before running it
  • I wrote a Promise by hand with both resolve and reject
  • I timed sequential against Promise.all and saw the difference
  • Every async function I wrote today has a catch and a finally

Quick recap

One thread: JavaScript never waits, it hands slow work out and carries on · You cannot return a value that has not arrived yet · .then/.catch flattened callbacks; async/await removed the nesting · Promise.all for independent work, sequential await only when dependent · Loading, error and empty are three states you must design, not one

Tomorrow, Day 8: the thing you have been waiting for. Real HTTP requests to real APIs, and what all those status codes mean.

Learn this with a real instructor

Free lessons take you a long way on your own. The full programmes add live classes, assignments, Skill Points and a certificate — built for African students.