Обработка ошибок API в React

Обработка ошибок API в React

19 октября 2022 г.

Введение

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

Окончательное решение TL/DR

https://gist.github.com/pivaszbs/b44e534a0fc01efb6a1b8ba55011c40b?embedable=true

Пошаговое объяснение

Определить требования (мой пример)

  1. Ошибка должна быть доступна извне (обработчик не должен ее потреблять). Это важно

потому что каждый раз, когда вы прячете ошибки за абстракциями, у вас потом возникают большие трудности. 2. Обработчик должен уметь работать с Zero-конфигурацией (настроены по умолчанию). Это важно, потому что с ним легче читать и работать

Что нужно сделать

  1. Согласитесь с бэкэндом по поводу общего формата ошибки, вы можете выбрать один из универсальных описанных ниже

// Backend error format
type ApiError = {
    message: string
}

  1. Создайте общий обработчик запросов, который можно настроить, например:

type Config = { errorHandler: (errors: ApiError[]) => void }
// hook for common usage
const useQuery = ({ errorHandler = defaultErrorHandler, url }: { url: string } & Partial<Config>) => {
    const [errors, setErrors] = useState();
    const [data, setData] = useState();
    const [loading, setLoading] = useState();

    useEffect(() => {
        setLoading(true);
        fetch(url)
            .then(data => data.json())
            .then(({ statusCode, status, ...data }) => {
                if (statusCode === 404) {
                    const errors = [data.message];
                    errorHandler(errors);
                    setErrors(errors);
                } else {
                    setData(data);
                }
            })
            .catch(e => {
                setErrors([e])
            })
            .finally(() => {
                setLoading(false)
            })
    }, [url])

    return { errors, data, loading }
}

  1. Используйте общий обработчик запросов везде и будьте счастливы и радостны тем, что пользователи никогда больше не будут озадачены тем, что происходит с ошибками

машинопись const {порода} = useParams(); const {данные, ошибки} = useQuery({ URL-адрес: `https://my-random-url`, обработчик ошибок: console.error });

Мое объяснение реализации

  1. Я выбрал внутренний рабочий формат с Both, потому что иногда мне нужно извлечь ошибки с помощью разных функций и легко разложить свое решение на небольшие функции (RequestWrapper — простой пример монады Both). Кроме того, он лучше типизирован, чем метод catch, поэтому вы можете попробовать сделать это таким образом :)

type RequestWrapper<T, E extends ApiError> = [T, undefined] | [undefined, E[]];

const convertToEither = async <T, E extends ApiError>(req: Promise<T>): Promise<RequestWrapper<T, E>> => {
    try {
        return [await req, undefined];
    } catch (e) {
        return [undefined, [e]]
    }
}

  1. Мне сейчас лень реализовывать logger и toast, поэтому я просто издеваюсь над этим :)

```машинопись класс Тост { статический showError = console.error;

класс Loggger { статический logError = console.log } ```

3. Я не хочу обрабатывать и извлекать ошибки в одном месте, поэтому решил сделать функцию извлечения

```машинопись const extractErrors = async (обертка: RequestWrapper): Promise> ; => { const [рез, ошибки = []] = оболочка; if (res?.status === 'incorrect-field-values') { вернуть [, [...ошибки, ...рез.ошибки]] }

   return wrapper;

} ```

4. Я хочу передать свой обработчик ошибок извне, поэтому я настраиваю его фабрикой

```машинопись const defaultErrorHandler = (ошибки: E[]) => { error.forEach(ошибка => { Toast.showError(ошибка); })

const defaultErrorHandlerFabric = (errorHandler = defaultErrorHandler) => async (обертка: RequestWrapper) => { const [, ошибки] = оболочка; если (ошибки?.длина) { обработчик ошибок (ошибки)

   return wrapper;

} ```

5. Просто соберите все вместе

```машинопись const handleRequest = >(req: T, config: Required) => { вернуть convertToEither(req) .затем (конвертировать в Json) .тогда(извлечьОшибки) .then(defaultErrorHandlerFabric(config.errorHandler)) .тогда (регистратор по умолчанию) }

const useQuery = ({ errorHandler = defaultErrorHandler, url }: { url: string } & Partial) => { const [ошибки, setErrors] = useState(); const [данные, setData] = useState(); const [загрузка, setLoading] = useState();

   useEffect(() => {
       setLoading(true);
       handleRequest(fetch(url), { errorHandler })
           .then(([data, errors]) => {
               setErrors(errors);
               setData(data);
           })
           .finally(() => {
               setLoading(false)
           })
   }, [url])

   return { errors, data, loading }

} ```

Заключительные мысли

  1. Всегда старайтесь обрабатывать ошибки, пользователи не знают, как работает ваш сервер)
  2. Не прячьте некоторые действительно необходимые вещи под абстракцию (например, скрытие ошибок), лучше настройте их снаружи
  3. Не беспокойтесь, что все ваши запросы будут обрабатываться одним методом, ничего страшного (если страшно, помните, что вы рисуете все приложение с помощью react)
  4. Используйте любой из них для улучшения типов Typescript
  5. Попробуйте этот подход в своем приложении :)


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE