Пример специально показан «в лоб», чтобы было понятно, с чем именно safe-fetch помогает. В реальном коде никто не пишет по 10 try/catch на каждый запрос — и вот как раз для этого я и сделал библиотеку. Она убирает всю эту рутину и превращает код в:
const res = await safeFetch.get<User[]>('/api/users');
if (res.ok) render(res.data);
else logger.error(res.error);
То есть идея не в красоте примера, а в том, чтобы таких простыней больше не писать руками.
Вот, да 👍 Даже если бэк идеальный, по пути всё равно может встрять 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-вариант:
Думаю сегодня займусь!
Пример специально показан «в лоб», чтобы было понятно, с чем именно safe-fetch помогает. В реальном коде никто не пишет по 10
try/catch
на каждый запрос — и вот как раз для этого я и сделал библиотеку. Она убирает всю эту рутину и превращает код в:То есть идея не в красоте примера, а в том, чтобы таких простыней больше не писать руками.
safe-fetch
не запрещаетthen/catch
. 🙂Можно работать и так:
А если хочется именно чтобы бросало — есть
unwrap
:То есть хочешь — работаешь без
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-вариант:
и ещё ESLint-правило, чтобы не забывали проверять
.ok
.