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

В 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 →