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 is not a simple thing, it's got a wide variety of rules that must be followed. For example, a program must have access to a file in order 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. Take a 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. This was 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, 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 the callback was called in its call stack, which started inside the readFile()
function. In fact, this means that using try/catch in asynchronous code with callbacks is useless, this construction is simply not applicable here.
What does the code below display?
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!. This seems strange, given that the error occurred inside the readFile()
function, not in the callback. This 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). This means that 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, accordingly, you have to check for it manually. If we get a null
, there's no error, but if there's no null
there is an error. This is a very important 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 have to 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. It must be called, 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
});
});
});
};
The last call can be shortened. If there was no error at the very end, then calling cb(error3)
will work the same way as calling cb(null)
, which means that 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));
The Hexlet support team or other students will answer you.
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.
Programming courses for beginners and experienced developers. Start training for free
Our graduates work in companies:
From a novice to a developer. Get a job or your money back!
Sign up or sign in
Ask questions if you want to discuss a theory or an exercise. Hexlet Support Team and experienced community members can help find answers and solve a problem.