All streams
Search
Write a publication
Pull to refresh
10
1
Александр Михайлишин @Asouei

User

Send message

Пример специально показан «в лоб», чтобы было понятно, с чем именно safe-fetch помогает. В реальном коде никто не пишет по 10 try/catch на каждый запрос — и вот как раз для этого я и сделал библиотеку. Она убирает всю эту рутину и превращает код в:

const res = await safeFetch.get<User[]>('/api/users');
if (res.ok) render(res.data);
else logger.error(res.error);

То есть идея не в красоте примера, а в том, чтобы таких простыней больше не писать руками.

safe-fetch не запрещает then/catch. 🙂
Можно работать и так:

safeFetch.get<User[]>('/api/users')
  .then(r => r.ok ? r.data : Promise.reject(r.error))
  .then(users => render(users))
  .catch(err => toast(err.message));

А если хочется именно чтобы бросало — есть unwrap:

unwrap(safeFetch.get<User[]>('/api/users'))
  .then(users => render(users))
  .catch(err => toast(err.message));

То есть хочешь — работаешь без try/catch, хочешь — с классическим throw.

Сам по себе axios не «огромный», но он тащит за собой много лишнего кода: старые фичи, XHR-совместимость, трансформации и т.д

Вот, да 👍 Даже если бэк идеальный, по пути всё равно может встрять Cloudflare, прокси или вайфай-роутер и подсунуть html вместо json. safe-fetch как раз и нужен, чтобы фронт от этого не падал у юзера.

Тут важно разделять «чинить баг на бэке» и «не уронить фронт у пользователя». Конечно, если бек шлёт 200 с пустым телом или ломает JSON — это баг сервиса, и фиксить его надо там. Но реальность такая, что фронтенд вынужден с этим сталкиваться каждый день (все видели проекты, где это приходилось чинить костылями).

safe-fetch как раз не «перекладывает ответственность», а даёт безопасный слой: ошибки нормализуются, JSON-парсинг не падает в рантайме, ретраи с Retry-After не забивают сервер. Это не замена фиксу на бекенде, а страховка для фронта, чтобы у пользователя не превращалось всё в белый экран.

Если бек идеальный — либу можно и не подключать. Но пока мы живём в мире неидеальных API, проще один раз вынести эту логику в маленький клиент, чем писать сотни try/catch по коду.

Да, согласен, в статье я перечислил не все боли.

В safe-fetch эти пункты есть:
Total timeout (totalTimeoutMs), чтобы не зависнуть навсегда.
POST не ретраится по умолчанию, чтобы не дублировать заказы/платежи.
Retry-After учитывается (и секунды, и дата).
Content-Type проверяется перед JSON-парсингом.

По поводу «умных дефолтов» — это осознанное решение: безопасные настройки по умолчанию (идемпотентные ретраи, backoff + jitter), потому что именно на этих «простых» кейсах чаще всего стреляют себе в ногу. Но всё это можно отключить: retries: false, timeoutMs: 0, errorMap: e => e.

JSON возвращает null не для того, чтобы спрятать ошибку, а чтобы не кидать исключения. Хочешь строгого поведения — указываешь validate и получаешь ValidationError.

Что касается «навязывания модели ошибок» — базовый union (Network | Timeout | Http | Validation) минимален, дальше через errorMap можно раскрасить в любые доменные ошибки.

Так что задача либы — дать безопасную базу на ~3kb, а сложные вещи вроде стриминга или очередей можно навесить сверху, не таща в проект ещё один axios.

По сути вы собрали свой мини-axios: таймаут на одну попытку, линейные ретраи, ручной JSON-парсинг. Я собрала @asouei/safe-fetch ровно для того, чтобы такие вещи не приходилось писать заново и чтобы они работали безопаснее.

В вашем коде есть несколько проблемных мест:
Нет total timeout: вы прерываете только отдельную попытку, но вся операция с ретраями может зависнуть навсегда. У меня есть totalTimeoutMs, который гарантированно обрывает всю цепочку.
Ретраи POST: у вас повторяются любые 5xx, и POST может уйти дважды → дублирование сайд-эффектов. В safe-fetch по умолчанию ретраятся только идемпотентные методы (GET/HEAD).
Retry-After: ваш клиент его игнорирует, вы стучитесь в закрытую дверь. У меня заголовок учитывается, и пауза ровно такая, как просит сервер.
JSON-парсинг: JSON.parse у вас бросает исключение и попадает в catch как «Network». У меня не кидается — возвращается null, а строгая проверка делается через validate.
Content-Type: вы всегда парсите JSON, даже если сервер вернул text/csv. В safe-fetch это проверяется.
Backoff: у вас линейный retryDelay * attempt, у меня экспоненциальный с джиттером и верхним капом.
Ошибки: ваши kind не типизированы. У меня дискриминированный union (NetworkError | TimeoutError | HttpError | ValidationError) + errorMap для доменных (NotFoundError, AuthError и т.д.).

В результате: @asouei/safe-fetch закрывает те же задачи, что и ваш клиент, но делает это безопаснее, типобезопаснее и предсказуемо.

Спасибо за фидбек 🙌 Да, про «можно случайно передать не .data, а весь результат» - это верно, но тут как раз спасает TS: safeFetch.get<User[]> возвращает union, и напрямую в функцию, где ждут User[], его не передашь.

Согласен, что ergonomics можно упростить. Думаю добавить tuple-вариант:

const [users, err] = await safeFetch.tuple<User[]>('/api/users');

и ещё ESLint-правило, чтобы не забывали проверять .ok.

Information

Rating
1,559-th
Registered
Activity

Specialization

Frontend Developer
Middle
From 100,000 ₽
Git
JavaScript
HTML
CSS
React
TypeScript
Node.js
Webpack
Adaptive layout
NextJS