Чтение и запись файлов, получение данных по сети, выполнение HTTP запросов — всё это операции ввода/вывода. Через них программа взаимодействует с внешней средой. Внешняя среда — штука не простая, с большим количеством разнообразных правил, которые необходимо соблюдать. Например, для успешного чтения файла программа должна иметь к нему доступ. Для записи — свободное место на диске. Для выполнения запросов по сети нужно соединение с сетью. Подобных условий десятки, а то и сотни. Невыполнение хотя бы одного из них приводит к ошибке. Посмотрите на этот впечатляющий список из нескольких сотен всевозможных ошибок.
В JavaScript обработка ошибок работает через механизм исключений. Одни функции их возбуждают, другие обрабатывают через try..catch. Так было в синхронном коде. В асинхронном стандартный механизм уже не работает.
Подумайте над тем как отработает код ниже:
import fs from 'fs';
try {
// Пытаемся читать директорию, а это ошибка
fs.readFile('./directory', 'utf-8', () => {
callUndefinedFunction();
});
} catch (e) {
console.log('error!')
}
Так как try/catch работает только с кодом из текущего стека вызовов, то он не сможет перехватить то, что вызвалось в другом стеке. Поэтому мы не увидим сообщения error!, хотя сама ошибка на экране появится:
callUndefinedFunction();
^
ReferenceError: callUndefinedFunction is not defined
at ReadFileContext.fs.readFile [as callback] (/private/var/tmp/index.js:6:5)
Из вывода видно, что колбек вызвался в своем стеке вызовов, начавшемся внутри функции readFile()
. Фактически это означает, что использовать try/catch в асинхронном коде с колбеками — бесполезно, эта конструкция здесь просто неприменима.
Что выведет на экран код ниже?
import fs from 'fs';
try {
// Пытаемся читать директорию, а это ошибка
fs.readFile('./directory', 'utf-8', () => {
console.log('finished!');
});
} catch (e) {
console.log('error!');
}
Правильный ответ: finished!. Это кажется странным, учитывая что ошибка возникла внутри функции readFile()
, а не в колбеке. Это происходит потому, что содержимое функции readFile()
не принадлежит текущему стеку вызовов.
Асинхронные функции всегда имеют дело с внешней средой (операционной системой). Это значит, что любая асинхронная функция потенциально может завершиться с ошибкой. Причём не важно возвращает ли она какие-то данные или нет, ошибка может возникнуть всегда. Именно по этой причине колбеки всех асинхронных функций первым параметром принимают ошибку err и, соответственно, проверять её наличие придётся руками. Если пришёл null
, то ошибки нет, если не null
— есть. Это очень важное соглашение, которого придерживаются не только разработчики стандартной библиотеки, но и все разработчики сторонних решений.
fs.readFile('./directory', 'utf-8', (err, data) => {
// Любые ошибки чтения файла: доступ, отсутствие файла, директория вместо файла
// null неявно приводится к false, поэтому достаточно такой проверки,
// любой другой ответ трактуется как true
if (err) {
console.log('error!');
return; // guard expression
}
console.log('finished!')
});
В цепочке вызовов придётся делать проверку на каждом уровне:
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!');
});
});
});
Тот же самый код, помещенный внутрь функции, выглядит немного по-другому. Как только происходит ошибка, мы вызываем основной колбек и отдаём туда ошибку. Если ошибка не возникла, то мы всё равно вызываем исходный колбек и передаём туда null
. Вызывать его обязательно, иначе внешний код не дождётся окончания операции. Следующие вызовы больше не выполняются:
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); // не забываем последний успешный вызов
});
});
});
};
Последний вызов можно сократить. Если в самом конце не было ошибки, то вызов cb(error3)
отработает так же, как и cb(null)
, а значит, весь код последнего колбека можно свести к вызову cb(error3)
:
fs.writeFile(outputPath, `${data1}${data2}`, cb);
// что равносильно fs.writeFile(outputPath, `${data1}${data2}`, error3 => cb(error3));
Вам ответят команда поддержки Хекслета или другие студенты.
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
Наши выпускники работают в компаниях:
Зарегистрируйтесь или войдите в свой аккаунт