Even with promises, it's not always clear how to structure asynchronous code. In this lesson, we'll cover some practices that make it easier to write and analyze. Again, let us consider the task of merging two files:
import fs from 'fs';
const unionFiles = (inputPath1, inputPath2, outputPath, cb) => {
fs.readFile(inputPath1, 'utf-8', (error1, data1) => {
if (error1) {
cb(error1);
return;
}
fs.readFile(inputPath2, 'utf-8', (error2, data2) => {
if (error2) {
cb(error2);
return;
}
fs.writeFile(outputPath, `${data1}${data2}`, cb);
});
});
}
Now we'll do a refactoring and get some canonical code when working with promises. So, the first version:
import fsp from 'fs/promises';
const unionFiles = (inputPath1, inputPath2, outputPath) => {
// Promises should always come back and be built in a chain
const result = fsp.readFile(inputPath1, 'utf-8')
.then((data1) => {
const promise = fsp.readFile(inputPath2, 'utf-8')
.then((data2) => fsp.writeFile(outputPath, `${data1}${data2}`));
return promise;
});
return result; // This is promise
};
The good news is that the code is clearer and shorter. Also, error handling is gone, since promises handle errors automatically, and if the code calling a function wants to catch them, it does so using the catch()
method. But the bad news is that the code is still structured like a staircase, just like callbacks. This code doesn't take into account the nature of the promises associated with the return of then()
:
import fsp from 'fs/promises';
const unionFiles = (inputPath1, inputPath2, outputPath) => {
const result = fsp.readFile(inputPath1, 'utf-8')
.then((data1) => fsp.readFile(inputPath2, 'utf-8'))
// then below is taken from the readFile promise
.then((data2) => fsp.writeFile(outputPath, `${<how to get here data1?>}${data2}`));
return result;
};
This version is flat, which is the kind of code to strive for when using promises. But it does present one problem. Whenever you need to retrieve data from somewhere in the chain below, you have to pass it through the whole chain. In the example above, this is the result of reading the first file.
The variable data1
is not available where the file is written. The way out of this situation is to create variables through which the data can be passed:
import fsp from 'fs/promises';
const unionFiles = (inputPath1, inputPath2, outputPath) => {
let data1;
return fsp.readFile(inputPath1, 'utf-8')
.then((content) => {
data1 = content;
})
.then(() => fsp.readFile(inputPath2, 'utf-8'))
.then((data2) => fsp.writeFile(outputPath, `${data1}${data2}`));
};
It's not as pretty, but flat. The advantage of this approach becomes more obvious as the number of promises increases. Also, you don't always need to pass data.
Controlling asynchronous operations
A common mistake when working with promises is losing control. Look at the slightly modified code from the examples above:
import fsp from 'fs/promises';
// What is missing here?
const unionFiles = (inputPath1, inputPath2, outputPath) => {
const result = fsp.readFile(inputPath1, 'utf-8')
.then((data1) => {
const promise = fsp.readFile(inputPath2, 'utf-8')
.then((data2) => fsp.writeFile(outputPath, `${data1}${data2}`));
});
return result;
};
This code works in many situations, but it contains a serious bug. The promise inside the promise
constant isn't returned outside. The promise chain is broken. Any error in this promise will go unnoticed by external code. There's no guarantee that this promise will execute at the time the calling code expects it.
Violating the continuity of asynchronous operations is a common error, even among experienced programmers. In some situations, this error is immediately noticeable; in others, the code behaves oddly. Sometimes it runs OK; in others, it crashes with random errors.
To prevent this, you need to make sure that asynchronous operations aren't interrupted. The completion of each operation must lead to some kind of response. There must always be something waiting for that moment.
Dynamic chain
Sometimes we don't know the number of asynchronous operations in advance, but they must execute one after the other. We can solve the problem by using loops or folding. Whichever way you choose, the principle of building a chain doesn't change. A chain of promises will always look like then().then().then()...
.
The only nuance you need to consider is the initial promise from which the chain starts. If such a promise doesn't exist, it can be created using the Promise.resolve()
function. It will return a promise that doesn't do anything, but you can use it to start folding. Below is an example that reads a set of files sequentially and returns an array of their contents:
const filePaths = /* list of paths to files */;
// This function takes an optional value as input,
// which appears in promise.then((<here>) => ...)
// In this case, the initial value is an array
// accumulating data from files
const initPromise = Promise.resolve([]);
// It's the function that is passed in, it's not its call
const promise = filePaths.reduce((acc, path) => {
// The accumulator is always a promise containing an array with the contents of the files
const newAcc = acc.then((contents) =>
// Reading the file and add its data to the accumulator
fsp.readFile(path).then((data) => contents.concat(data)));
return newAcc;
}, initPromise);
// Continue processing if necessary
promise.then((contents) => /* Processing all data obtained from the files */);
Are there any more questions? Ask them in the Discussion section.
The Hexlet support team or other students will answer you.
For full access to the course you need a professional subscription.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.