Обработка ошибок в асинхронных функциях
В 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
.