Чтение и запись файлов, получение данных по сети, выполнение HTTP запросов — всё это операции ввода/вывода. Через них программа взаимодействует с внешней средой. Внешняя среда — штука сложная, с большим количеством разнообразных правил, которые необходимо соблюдать. Например, для успешного чтения файла программа должна иметь к нему доступ. Для записи — свободное место на диске. Для выполнения запросов по сети нужно соединение с сетью. Подобных условий десятки, а то и сотни. Невыполнение хотя бы одного из них приводит к ошибке. Посмотрите на этот впечатляющий список возможных ошибок. В нём несколько сотен всевозможных ошибок!

В JavaScript обработка ошибок работает через механизм исключений. Одни функции их возбуждают, другие обрабатывают через try..catch. Так было в синхронном коде. В асинхронном стандартный механизм уже не работает.

Подумайте над тем как отработает код ниже:

import fs from 'fs';

try {
  // Пытаемся читать директорию, а это ошибка
  fs.readFile('./file', '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, data3) => {
      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));
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →