Synchronous vs Asynchronous Code
AI Using GitHub Copilot in VS Code
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.
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');
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:
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.existsSync() together with promise‑based I/O functions for reading and writing the files.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..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.This example builds upon the one from the previous chapter about the Meaning of Life .
whatIsTheMeaningOfLife() promise as in the previous chapter, but without the interval.<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:
.then() and .catch() returns a new promise.return anything, it implicitly returns undefined..then() or .catch() in the chain receives undefined as its input value.Now, let’s break down the code example:
The function whatIsTheMeaningOfLife() is similar to that of the previous chapter, but now simplified to no longer feature an interval timer. It still returns a promise that is either resolved or rejected.
The new function ask() calls whatIsTheMeaningOfLife() and processes the returned promise in a single promise chain.
The ask() function starts by creating a text variable that will hold the text to be logged. It is initialized with a time stamp.
In case of success, the first .then() in the chain appends the answer to the text variable and returns a promise that is resolved to undefined.
In case of failure, the first .catch() appends the error message to the text variable and returns a promise that is resolved to undefined.
In our first .then() and first .catch() we do not return anything, so the second .then() does not receive the answer or error. Instead, it uses the outer text variable.
The second .then() always runs.
If the original promise fails, the first .catch() handles the error and returns a resolved promise (because it does not throw or return a rejected promise). That means the chain continues as if everything is fine, and so the second .then() is called. This could be visualized as follows:
whatIsTheMeaningOfLife()resolves → first.then()→ second.then()→appendFile()whatIsTheMeaningOfLife()rejects → first.catch()→ second.then()→appendFile()
The callback of the second .then() appends the content of the text variable from the outer scope to the answers.log file, using the promises-based Node.js function fsPromises.appendFile(). The promise returned by .appendFile() is returned by the callback.
<aside> ❗
appendFile means “add to the end of the file”. If the file does not exist yet, Node will create it.
</aside>
Please be aware that I/O operations may fail. The second .catch() in the chain is used to report potential errors from .appendFile() to the console.
<aside> ❗
Notice that text is defined outside the promise chain. Both the success handler and the error handler update the same text variable, and the final .then() uses that shared value.
</aside>
Expand to reveal a callbacks-based equivalent of this example.
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.

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