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

Synchronous vs Asynchronous

In this section we will revisit file I/O, first using a synchronous approach and then rewriting the same logic with promises and asynchronous functions. Both versions produce the same result, but they interact with the event loop in very different ways: the synchronous version blocks execution while it reads and writes files, whereas the asynchronous version allows other work to continue in the meantime. As you read through the examples, focus on how control flow, error handling, and readability change when we move from synchronous to asynchronous file operations.

Example 1: Synchronous File I/O

This example using synchronous file I/O, though slightly enhanced to include error handling, was introduced in week 6. The function backup() takes a filename as parameter and creates a backup copy if the file exists. Note that this version blocks the event loop while reading/writing

import fs from 'node:fs';

function backup(filename) {
  if (!fs.existsSync(filename)) {
    return;
  }

  try {
    const content = fs.readFileSync(filename, 'utf8');
    const backupName = `${filename}.backup`;
    fs.writeFileSync(backupName, content);
    console.log(`Backed up ${filename} to ${backupName}`);
  } catch (err) {
    console.error('Error during backup:', err);
  }
}

backup('important-data.txt');

Example 2: Asynchronous File I/O

Here, we will rewrite example 1 to use promises and asynchronous file I/O.

import fsPromises from 'node:fs/promises';

function backup(filename) {
  const backupName = `${filename}.backup`;

  return fsPromises
    .access(filename)
    .then(() => fsPromises.readFile(filename, 'utf8'))
    .then((content) => fsPromises.writeFile(backupName, content))
    .then(() => {
      console.log(`Backed up ${filename} to ${backupName}`);
    })
    .catch((err) => {
      if (err.code === 'ENOENT') {
        // File does not exist, do nothing
        return;
      }
      console.error('Error during backup:', err);
    });
}

backup('important-data.txt');

Notes:

  1. Node.js does not include a promise-based version for the existsSync() function. Instead we use the fsPromises.access() function here. This will return a rejected promise if the file does not exist. The error object from the rejected promise will contain a code field (a string value) that we can inspect to determine the type of error. The error code 'ENOENT' indicates that the file does not exist. In the synchronous version we did not consider it to be an error if the file does not exist. To mimic the same behaviour, we silently return without logging an error when the file does not exist. All other errors will however be logged to the console.
  2. Since testing for file existing in a synchronous way is not likely to cause a noticeable blocking issue we could potentially still use existsSync() together with promise‑based I/O functions for reading and writing the files.
  3. The backup() function returns a promise that settles when the backup has finished (or failed). This allows you to await backup('...') or .then() on it later.
  4. The callback functions of the second and third .then() hold a reference to the backupName variable in their outer scope. Recall from Week 6 (Advanced topics) that such functions are said to form closures.

Example 3: The Meaning of Life, revisited

This example builds upon the one from the previous chapter about the Meaning of Life .

<aside> 💭

This example is meant to run in Node.js, not in the browser, because it uses the built‑in fs module to write a file.

</aside>

import fsPromises from 'node:fs/promises';

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(() => {
      return fsPromises.appendFile('answers.log', text + '\\n');
    })
    .catch((err) => {
      console.error('Failed to write to log file:', err);
    });
}

ask();

Before we discuss the code in detail, It is important to understand how promise chains work:

Now, let’s break down the code example:

Asking more than once

You may have noticed that the ask() function returns the promise returned by the promise chain. Technically, that is the promise returned by the final .catch(), a promise that resolves to the value undefined. Consequently, we could create another chain to ask the question more then once, logging the answer each time:

ask()
	.then(() => ask())
	.then(() => ask());
	
// Or, more succintly:

ask().then(ask).then(ask);

We do not need a .catch() at the end of this chain as there is no way imaginable that this chain might encounter an error. Any possible errors stay within the confines of the ask() function: they are completely handled there.


CC BY-NC-SA 4.0 Icons

*https://hackyourfuture.net/*

Found a mistake or have a suggestion? Let us know in the feedback form.