Despite all the conveniences, promises are not the pinnacle of programming evolution. Let's look at the drawbacks they add:
- It has its error handling, which bypasses try/catch. It means that both types of error handling appear in the code in odd combinations.
- Sometimes, you need to pass data down the chain from the highest levels, and it's not very convenient to do that with promises. We need to create variables outside the promise
- It's still easy to end up with nested code when using promises if you're not paying attention.
All of these problems can be solved by the async/await mechanism, which makes code with promises look even more like synchronous code.
Let's recall our task of merging two files:
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}`));
};
Now let's look at the same code using async/await. Note that async/await works with promises:
import fsp from 'fs/promises';
const unionFiles = async (inputPath1, inputPath2, outputPath) => {
// This is a major point. Just as in the example above, these requests execute strictly one after the other
// It doesn't block the program, but it means that another code executes during these requests
const data1 = await fsp.readFile(inputPath1, 'utf-8');
const data2 = await fsp.readFile(inputPath2, 'utf-8');
await fsp.writeFile(outputPath, `${data1}${data2}`);
};
This version looks almost exactly like the synchronous version. The code is so simple, it's hard to believe it's not synchronous. Let's break it down step by step.
The first thing we see is the keyword async
before the function definition. It means that this function always returns a promise: const promise = unionFiles(...)
. And now we don't need to return the result of this function explicitly, it will become a promise anyway.
The keyword await
is used inside the function before calling functions that also return promises. If the result of this call is assigned to a variable or constant, the result of the call is written to it. If there is no assignment to a variable or constant, as in the last await
call, then the operation waits to be executed without using its result.
As with the promises, asynchrony in this case guarantees that the program won't lock up while waiting for calls to complete. It can continue doing something else, but not in this function. But it does not guarantee that things will happen in parallel.
Also, successive await
calls within the same function always run in strict order. The easiest way to understand this is to think of the code as a chain of promises, where each successive operation takes place inside then
.
What about error handling? Now all you have to do is add the usual try/catch and it catches the errors:
import fsp from 'fs/promises';
const unionFiles = async (inputPath1, inputPath2, outputPath) => {
try {
const data1 = await fsp.readFile(inputPath1, 'utf-8');
const data2 = await fsp.readFile(inputPath2, 'utf-8');
await fsp.writeFile(outputPath, `${data1}${data2}`);
} catch (e) {
console.log(e);
throw e; // Throwing it again, because the code which calls the function should catch the errors
}
};
However, when executing promises in parallel, you can't do without the Promise.all
function:
const unionFiles = async (inputPath1, inputPath2, outputPath) => {
// These calls start reading almost simultaneously and don't wait for each other
const promise1 = fsp.readFile(inputPath1, 'utf-8');
const promise2 = fsp.readFile(inputPath2, 'utf-8');
// Now we wait for them both to finish
// The data can be unpacked straight away
const [data1, data2] = await Promise.all([promise1, promise2]);
await fsp.writeFile(outputPath, `${data1}${data2}`);
};
In conclusion, the async/await mechanism makes the code as flat and synchronous as possible. It makes it possible to use try/catch and easily manipulate data from asynchronous operations:
// The code with callbacks
import fs from 'fs';
fs.readFile('./first', 'utf-8', (error1, data1) => {
if (error1) {
console.log('boom!');
return;
}
fs.readFile('./second', 'utf-8', (error2, data2) => {
if (error2) {
console.log('boom!');
return;
}
fs.writeFile('./new-file', `${data1}${data2}`, (error3) => {
if (error3) {
console.log('boom!');
}
});
});
});
// The code with promises
import fsp from 'fs/promises';
let data1;
fsp.readFile('./first', 'utf-8')
.then((d1) => {
data1 = d1;
return fsp.readFile('./second', 'utf-8');
})
.then((data2) => fsp.writeFile('./new-file', `${data1}${data2}`))
.catch(() => console.log('boom!'));
// The code on async/await
import fsp from 'fs/promises';
// In real life, it's better to read files in parallel, like in this function
const data1 = await fsp.readFile('./first', 'utf-8');
const data2 = await fsp.readFile('./second', 'utf-8');
await fsp.writeFile('./new-file', `${data1}${data2}`);
Recommended materials
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.