Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Обработка ошибок JS: Асинхронное программирование

Чтение и запись файлов, получение данных по сети, выполнение 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, 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));

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
Об обучении на Хекслете

Для полного доступа к курсу нужна профессиональная подписка

Профессиональная подписка откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
900
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 120 курсов, 2000+ часов теории
  • 900 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Фронтенд-разработчик
Профессия
Разработка фронтенд-компонентов веб-приложений
29 сентября 8 месяцев
Иконка программы Node.js-разработчик
Профессия
Разработка бэкенд-компонентов веб-приложений
29 сентября 8 месяцев

Есть вопрос или хотите участвовать в обсуждении?

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»