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