Reading and writing files, retrieving data over the network, and executing HTTP requests are all I/O operations. Through them, the program interacts with the external environment.
The external environment has a wide variety of rules that we must follow. For example, a program must have access to a file to read it successfully. For writing, it needs free space on the disk. To perform queries over the network, you need a connection to the network.
There are dozens or even hundreds of such conditions. Failure to do at least one of them will lead to an error. Look at this impressive list of several hundred errors of all kinds.
In JavaScript, error handling works using an exception mechanism. Some functions excite them, others process them through try..catch
.
It is the case with synchronous code. With asynchronous code, the standard mechanism no longer works.
Think about how the code below will work:
import fs from 'fs';
try {
// We try to read the directory and get an error
fs.readFile('./directory', 'utf-8', () => {
callUndefinedFunction();
});
} catch (e) {
console.log('error!')
}
Since try/catch
only works with code from the current call stack, it won't be able to intercept anything in another stack. Therefore, we won't see an error!
message, though the error itself will appear on the screen:
callUndefinedFunction();
^
ReferenceError: callUndefinedFunction is not defined
at ReadFileContext.fs.readFile [as callback] (/private/var/tmp/index.js:6:5)
You can see from the output that we have called the callback in its call stack, which started inside the readFile()
function. It means that using try/catch
in asynchronous code with callbacks is useless. This construction is just not applicable here.
Let's observe what the code below displays:
import fs from 'fs';
try {
// We try to read the directory and get an error
fs.readFile('./directory', 'utf-8', () => {
console.log('finished!');
});
} catch (e) {
console.log('error!');
}
The correct answer is finished!
. It seems odd, given that the error occurred inside the readFile()
function, not in the callback. It happens because the contents of the readFile()
function don't belong to the current call stack.
Asynchronous functions always deal with the external environment (operating system). In other words, any asynchronous function could potentially end with an error. It doesn't matter whether it returns some data or not; an error can always occur.
This is the reason why all asynchronous functions take err
as the first parameter, and you have to check for it manually. If we get a null
, there's no error. This is a major agreement, to which not only the developers of the standard library adhere but also all developers of third-party solutions:
fs.readFile('./directory', 'utf-8', (err, data) => {
// Any file reading errors: access, no file, directory instead of file
// Null can implicitly be reduced to false, so this check is sufficient,
// Any other answer is treated as true
if (err) {
console.log('error!');
return; // guard expression
}
console.log('finished!')
});
In the call chain, you should do a check at each level:
import fs from 'fs';
fs.readFile('./first', 'utf-8', (error1, data1) => {
if (error1) {
console.log('error in first file')
return;
}
fs.readFile('./second', 'utf-8', (error2, data2) => {
if (error2) {
console.log('error in second file')
return;
}
fs.writeFile('./new-file', `${data1}${data2}`, (error3) => {
if (error3) {
console.log('error during writing')
return;
}
console.log('finished!');
});
});
});
The same code placed inside the function looks a little different. As soon as an error occurs, we call the main callback and send the error there. If there's no error, we still call the original callback and pass null
into it. We must call it. Otherwise, the external code won't wait until the end of the operation. The following calls are no longer made:
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}`, (error3) => {
if (error3) {
cb(error3);
return;
}
cb(null); // do not forget the last successful call
});
});
});
};
We can shorten the last call. If there was no error at the very end, then calling cb(error3)
will work the same way as cb(null)
. So, all the code of the last callback can be reduced to calling cb(error3)
:
fs.writeFile(outputPath, `${data1}${data2}`, cb);
// Which is equivalent to fs.writeFile(outputPath, `${data1}${data2}`, error3 => cb(error3));
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.