Саша БеспоясовФотография автора

Обработка ошибок в асинхронных функциях

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

Асинхронные функции работают на промисах. Очень грубо говоря await внутри асинхронной функции как бы приостанавливает её выполнение, ждёт, когда зарезолвится промис, указанный после него, и возвращает результат.

И на первый взгляд в асинхронных функциях есть проблемы с обработкой ошибок.

Промисы

Допустим, у нас есть функция loadPost, которая получает с сервера статью и работает на промисах. Метод fetch отправляет запрос на указанный адрес, и возвращает промис, который мы в дальнейшем можем обработать.

Если всё хорошо, получаем данные с помощью .json, который тоже возвращает промис. Если что-то пошло не так, отловим ошибку в .catch.

const loadPost = (postId) =>
  fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
    .then((response) => response.json())
    .then((data) => console.log(data.title))
    .catch((e) => console.log(`Error! ${e}`));

loadPost(1);

Асинхронные функции

Попробуем переписать эту функцию, используя async/await. Делаем её асинхронной с помощью ключевого слова async, без этого бы не сможем использовать await внутри.

Вторая строка совершает запрос, await «разворачивает» промис и возвращает значение, которое записывается в переменную response. Третья строка получает джейсон и записывает значение в переменную data.

const loadPost = async (postId) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
  const data = await response.json();
  console.log(data.title);
};

loadPost(1);

Пока запрос проходит без ошибок у нас всё хорошо. Но если что-то пойдёт не так, выскочит исключение:

Uncaught (in promise) TypeError: Failed to fetch

Обработка ошибок

Окей, используем try-catch, чтобы отловить ошибку:

const loadPost = async (postId) => {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
    const data = await response.json();
    console.log(data.title);
  } catch (e) {
    console.log(`Error! ${e}`);
  }
};

loadPost(1);

Вроде нормально, но функция стала больше, да и запросы могут быть разными, а каждый раз писать try-catch запарно.

Вспоминаем, что асинхронная функция возвращает промис, поэтому можно использовать .catch, чтобы отловить ошибку:

const loadPost = async (postId) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
  const data = await response.json();
  console.log(data.title);
};

loadPost(1).catch((e) => console.log(`Error! ${e}`));

Это решает проблему с повторами try-catch, но не решает проблему с дублированием кода. Здесь может помочь функция высшего порядка.

const loadPost = async (postId) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
  const data = await response.json();
  console.log(data.title);
};

// функция высшего порядка по очереди «запоминает»
// обработчик ошибок, сам запрос, аргументы для запроса
const tryCatchWrapper =
  (handleError) =>
  (reqFn) =>
  (...args) =>
    reqFn(...args).catch(handleError);

// обработчик ошибок
const handleError = (e) => console.log(`Error! ${e}`);

// запомнили функцию для обработки ошибок
const errorHandlerWrapper = tryCatchWrapper(handleError);

// запомнили, какой запрос хотим выполнить
const safelyLoadPost = errorHandlerWrapper(loadPost);

// выполняем этот запрос
safelyLoadPost(1);

Запрос остался без изменений, но добавилась функция tryCatchWrapper. Она принимает как аргумент функцию handleError, которая будет обрабатывать исключения, и возвращает новую функцию.

Эта новая функция принимает функцию запроса, который мы собираемся отправлять, и возвращает ещё одну функцию. Эта конечная функция принимает параметры, которые будет передавать в функцию запроса, когда мы вызовем её.

По-умному это ещё называю каррированием: когда мы из одной функции с несколькими аргументами делаем несколько функций, которые принимают по одному аргументу. Так мы можем «запоминать» аргументы, не вызывая функцию сразу, а вызывать её потом.

Всё это вместе позволяет написать несколько обработчиков ошибок, которые будут применять разные функции в зависимости от наших целей. Например, если мы захотим использовать другой обработчик для какого-то запроса, мы можем передать как аргумент другую функцию:

// первый обработчик
const handleError = (e) => console.log(`Error! ${e}`);
const errorHandlerWrapper = tryCatchWrapper(handleError);

// другой
const handleErrorDifferently = (e) => console.log(`Wow! It is all different now`);
const otherErrorHandlerWrapper = tryCatchWrapper(handleErrorDifferently);

И при этом не будет дублирования кода, потому что вся обработка находится внутри tryCatchWrapper.

Ссылки по теме

Предыдущий пост: Краков, апрель 2018Следующий пост: Как сделать документацию полезной