Week 10 - Asynchronous programming

Synchronous vs Asynchronous Code

Callbacks

Promises

Asynchronous File I/O

Event Loop

Fetch API

Async/Await and Promise.all()

AI Using GitHub Copilot in VS Code

Practice

Assignment

Back to core program

Async/Await and Promise.all()

In the previous chapters you learned how to:

On this page we will:

<aside> ⚠️

async / await does not replace promises. It is just a more convenient way to use them. Under the hood, everything is still promises and microtasks.

</aside>

From Promises to async/await

Let us start from a pattern you already know from the previous chapters:

function whatIsTheMeaningOfLife() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.5) {
        resolve(42);
      } else {
        reject(new Error('Come back in 7.5 million years and ask me again!'));
      }
    }, 1000);
  });
}

function ask() {
  let text = new Date().toISOString() + ' ';

  return whatIsTheMeaningOfLife()
    .then((result) => {
      text += `The answer is ${result}`;
    })
    .catch((err) => {
      text += `Error: ${err.message}`;
    })
    .then(() => {
      console.log(text);
    });
}

ask();

This code:

The same logic with async/await

We can write the same logic using async / await as follows:

function whatIsTheMeaningOfLife() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.5) {
        resolve(42);
      } else {
        reject(new Error('Come back in 7.5 million years and ask me again!'));
      }
    }, 1000);
  });
}

async function ask() {
  let text = new Date().toISOString() + ' ';

  try {
    const result = await whatIsTheMeaningOfLife();
    text += `The answer is ${result}`;
  } catch (err) {
    text += `Error: ${err.message}`;
  }

  console.log(text);
}

ask();

What changed?

Conceptually:

<aside> ⚠️

You can only use await inside a function marked with async (or at the top level in modern Node with ESM modules). Using await in a normal function results in a syntax error.

</aside>

How async functions relate to promises

Even though they look like ordinary functions, async functions always return a promise.

async function add(a, b) {
  return a + b;
}

const resultPromise = add(2, 3);
console.log(resultPromise); // Promise { 5 }

resultPromise.then((value) => {
  console.log('Value:', value); // Value: 5
});

Rules to remember:

This connects directly to the mental model from the Promises chapter: async / await is just “syntax sugar” around Promise objects and .then() / .catch().

Using async/await with fetch()

In the Fetch API chapter you wrote code like this:

const url = '<https://jsonplaceholder.typicode.com/posts/1>';

fetch(url)
  .then((response) => {
    if (!response.ok) {
      throw new Error(`Request failed with status ${response.status}`);
    }
    return response.json();
  })
  .then((data) => {
    console.log('Data:', data);
  })
  .catch((error) => {
    console.error('Error while fetching:', error.message);
  });

Let us now rewrite this as an async function:

const url = '<https://jsonplaceholder.typicode.com/posts/1>';

async function fetchPost() {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Request failed with status ${response.status}`);
    }

    const data = await response.json();
    console.log('Data:', data);
  } catch (error) {
    console.error('Error while fetching:', error.message);
  }
}

fetchPost();

Compare the two versions:

Under the hood, both versions behave the same with respect to:

Async/await and control flow

Because await can be used almost anywhere inside an async function, we can mix synchronous logic and asynchronous calls in a way that often feels more natural.

Consider this example that first checks some input synchronously and then fetches data:

async function fetchUserPost(userId, postId) {
  if (typeof userId !== 'number' || typeof postId !== 'number') {
    throw new Error('userId and postId must be numbers');
  }

  const url = `https://jsonplaceholder.typicode.com/posts/${postId}`;
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Request failed with status ${response.status}`);
  }

  const post = await response.json();
  return post;
}

fetchUserPost(1, 1)
  .then((post) => {
    console.log('Post:', post.title);
  })
  .catch((error) => {
    console.error('Error:', error.message);
  });

Here you see all the concepts you have already practiced, just with different syntax:


Running multiple fetch() calls in parallel with Promise.all()

So far we have mostly done one asynchronous operation at a time.

But in many real‑world scenarios you want to:

That is where Promise.all() comes in.

What Promise.all() does

Promise.all() takes an array of promises and returns a single promise that:

const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);

Promise.all([p1, p2, p3]).then((values) => {
  console.log(values); // [1, 2, 3]
});

If any of the promises fails:

const ok = Promise.resolve('ok');
const fail = Promise.reject(new Error('Boom'));

Promise.all([ok, fail])
  .then((values) => {
    // This will not run
  })
  .catch((err) => {
    console.error('At least one promise failed:', err.message);
  });

Why this matters for fetch()

fetch() returns a promise. That means you can pass multiple fetch() calls into Promise.all() to:

This is usually much faster than waiting for each request one by one.

Example: fetching multiple posts in parallel (Promise chains)

First, here is a promise‑chain version using the Fetch API and Promise.all():

const urls = [
  '<https://jsonplaceholder.typicode.com/posts/1>',
  '<https://jsonplaceholder.typicode.com/posts/2>',
  '<https://jsonplaceholder.typicode.com/posts/3>',
];

Promise.all(urls.map((url) => fetch(url)))
  .then((responses) => {
    responses.forEach((response) => {
      if (!response.ok) {
        throw new Error(`Request failed with status ${response.status}`);
      }
    });

    // Turn each Response into a promise for its JSON body
    return Promise.all(responses.map((response) => response.json()));
  })
  .then((posts) => {
    console.log('Received posts:');
    posts.forEach((post) => {
      console.log('-', post.title);
    });
  })
  .catch((error) => {
    console.error('Error while fetching posts:', error.message);
  });

The same idea with async/await

Now the exact same idea using async / await:

const urls = [
  '<https://jsonplaceholder.typicode.com/posts/1>',
  '<https://jsonplaceholder.typicode.com/posts/2>',
  '<https://jsonplaceholder.typicode.com/posts/3>',
];

async function fetchMultiplePosts() {
  try {
    // Start all fetches in parallel
    const responses = await Promise.all(urls.map((url) => fetch(url)));

    // Check statuses
    for (const response of responses) {
      if (!response.ok) {
        throw new Error(`Request failed with status ${response.status}`);
      }
    }

    // Read all bodies in parallel
    const posts = await Promise.all(
      responses.map((response) => response.json())
    );

    console.log('Received posts:');
    for (const post of posts) {
      console.log('-', post.title);
    }
  } catch (error) {
    console.error('Error while fetching posts:', error.message);
  }
}

fetchMultiplePosts();

Key ideas:

<aside> 💡

</aside>

Promise.all(), visualized

We have include an example in the Learning-Resources GitHub repository that visualises the effect of Promise.all(). You can find that example here:

It is a simple web application that show three cats walking across the screen from left to right, while pausing in the middle to do a little dance. Here is what it looks like when you press the Start button:

catwalks-all.gif

Here is an extract of the code for this example which we will break down below.