Этот мем смешной, пока не осознаешь, что в реальных проектах мы именно так и поступаем. Только заворачиваем не весь код сразу, а каждый HTTP-запрос по отдельности.
Пишешь fetch и рефлекторно добавляешь try/catch. Где-то словил TypeError, где-то таймаут, где-то сервер вернул 500. В итоге половина кода превращается в кашу проверок, а другая половина — в обработчики ошибок.
Я годами так делал, пока не понял: проблема не в том, что мы ловим ошибки. Проблема в том, что fetch заставляет нас их ловить везде и всегда.
Так появилась библиотека @asouei/safe-fetch. Ее задача проста: убрать try/catch из проектов навсегда.
Проблемы, которые достали всех
Помните эту красоту?
try { const res = await fetch('/api/users'); if (!res.ok) { throw new Error(`HTTP ${res.status}: ${res.statusText}`); } const data = await res.json(); // что-то делаем с data } catch (e) { // а тут ловим все подряд: таймауты, 404, проблемы с сетью console.error('Что-то пошло не так:', e.message); }
Через месяц в проекте половина функций выглядит именно так. А проблемы одни и те же:
-
fetchкидает исключения только на сетевые сбои. 404 и 500 надо ловить руками -
Нет «общего таймаута» на операцию. Только костыли с
AbortController -
Логика повторов? Пиши сам или тащи тяжелый axios
-
Ошибки не типизированы. В TypeScript приходится гадать что в
e.message
Что я хотел получить
Три простые вещи:
1. Никаких throw
Каждый вызов возвращает результат с понятным флагом ok.
2. Нормализованные ошибки
Вместо загадочного e.message — четкие типы: NetworkError, TimeoutError, HttpError, ValidationError.
3. Фишки из коробки
Общий таймаут, умные ретраи, поддержка Retry-After.
Вот как это выглядит:
import { safeFetch } from '@asouei/safe-fetch'; const result = await safeFetch.get<{ users: User[] }>('/api/users'); if (result.ok) { console.log(result.data.users); } else { console.error(result.error.name); // NetworkError | TimeoutError | ... }
Результат всегда предсказуемый: либо { ok: true, data }, либо { ok: false, error }.
Ни одного try/catch в бизнес-логике.
Что под капотом
Двойные таймауты
Можно задать timeoutMs для одной попытки и totalTimeoutMs для всей операции:
const api = createSafeFetch({ timeoutMs: 5000, // 5с на попытку totalTimeoutMs: 30000 // 30с всего (включая ретраи) });
Умные ретраи
По умолчанию повторяются только GET и HEAD — это защищает от случайных дубликатов POST-запросов:
const result = await safeFetch.get('/api/flaky', { retries: { retries: 3, baseDelayMs: 300 // экспоненциальный backoff } });
Поддержка Retry-After
Если сервер вернул 429 с заголовком — библиотека сама подождет:
// Сервер: 429 Too Many Requests, Retry-After: 60 // safe-fetch: ждем ровно 60 секунд
Validation без исключений
Можно подключить Zod или другую схему:
const result = await safeFetch.get('/user/123', { validate: (data) => UserSchema.safeParse(data).success ? { success: true, data } : { success: false, error: 'Invalid user' } });
Реальная польза
До: кодовая база из ада
async function getUsers() { try { const res = await fetch('/api/users'); if (!res.ok) throw new Error(`${res.status}`); return await res.json(); } catch (e) { logger.error('Users fetch failed', e); throw e; // пробрасываем дальше } } async function createUser(data) { try { const res = await fetch('/api/users', { method: 'POST', body: JSON.stringify(data) }); if (!res.ok) throw new Error(`${res.status}`); return await res.json(); } catch (e) { logger.error('User creation failed', e); throw e; } }
После: чистый код
const api = createSafeFetch({ baseURL: '/api', interceptors: { onError: (error) => logger.error('API error', error) } }); async function getUsers() { return api.get<User[]>('/users'); } async function createUser(data: NewUser) { return api.post<User>('/users', data); }
Весь error handling в одном месте. Никаких дублирующихся проверок.
История из практики
У меня был проект в небольшой команде, где мы работали с несколькими сторонними API. На бумаге всё выглядело просто: дергаем данные, отображаем в интерфейсе. Но реальность быстро всё усложнила.
Что пошло не так:
-
Один сервис периодически отвечал 500-ми ошибками
-
Другой любил возвращать пустые JSON-ы, хотя статус был 200
-
Иногда ответы зависали на десятки секунд, и пользователи жаловались, что «кнопка не работает»
В итоге код превратился в хаос из try/catch, таймеров с AbortController и кучи логов вроде «Request failed again». Мы даже обсуждали идею тащить axios, хотя никто не горел желанием добавлять ещё одну тяжелую зависимость.
В какой-то момент я собрался и сказал: «Хватит. Мы тратим больше времени на ловлю ошибок, чем на фичи». Так появился safe-fetch.
После перехода:
-
Весь error handling уехал в interceptors — стало понятно, где искать баги
-
Ретраи на GET реально спасли от флейки API (раньше мы просто рефрешили страницу)
-
Общий таймаут избавил от «вечных» спиннеров, когда пользователь ждал ответа, который никогда не придёт
-
В логах наконец появились внятные названия ошибок (
NetworkError,TimeoutError), а не загадочные «undefined»
Через пару недель мы заметили, что больше вообще не пишем try/catch вокруг запросов. И это стало огромным облегчением для всей команды.
Сравнение с конкурентами
|
Фича |
safe-fetch |
axios |
ky |
fetch |
|---|---|---|---|---|
|
Размер |
~3kb |
~13kb |
~11kb |
0kb |
|
Безопасные результаты |
✅ |
❌ |
❌ |
❌ |
|
Типизированные ошибки |
✅ |
❌ |
❌ |
❌ |
|
Общий таймаут |
✅ |
❌ |
❌ |
❌ |
|
Retry-After |
✅ |
❌ |
❌ |
❌ |
|
Zod-ready |
✅ |
❌ |
❌ |
❌ |
Установка и первые шаги
npm install @asouei/safe-fetch
Базовый пример:
import { safeFetch } from '@asouei/safe-fetch'; const users = await safeFetch.get<User[]>('/api/users'); if (users.ok) { console.log(users.data); } else { console.error(users.error.name, users.error.message); }
Для больших проектов:
import { createSafeFetch } from '@asouei/safe-fetch'; const api = createSafeFetch({ baseURL: 'https://api.example.com', headers: { 'Authorization': `Bearer ${token}` }, timeoutMs: 10000, retries: { retries: 2 } });
Для кого это
-
Команды, уставшие от непредсказуемых ошибок и дублирующего кода
-
Проекты с жесткими SLA, где важны таймауты и ретраи
-
TypeScript-кодбазы, где нужна точная типизация ошибок
-
Разработчики, которые хотят простоту fetch с production-готовностью
Что дальше
Библиотека уже готова к продакшену. В планах:
-
ESLint правила для паттерна
{ ok } -
Готовые адаптеры для React Query и SWR
-
Примеры для Next.js и Cloudflare Workers
Заключение
Вообще я создавал эту штуку для себя, чтобы самому было легче. Но вскоре понял, что она может быть полезна каждому — решил поделиться.
safe-fetch не пытается заменить axios или ky. Она решает одну задачу: делает fetch безопасным и предсказуемым. Никаких революций — просто убирает ту ежедневную боль, с которой мы все смирились.
Может, вы тоже устали объяснять джунам, почему нужно проверять res.ok? Или писать одинаковые обработчики ошибок в каждом API-методе? Если да — попробуйте. Возможно, через неделю вы уже не захотите возвращаться к старым паттернам.
А если найдете баги или захотите что-то улучшить — буду рад увидеть в Issues. В конце концов, эта библиотека родилась из реальных проблем, и лучше всего она растет от реального фидбека.
-
🌟 Библиотека добавлена в Awesome TypeScript — один из крупнейших мировых списков лучших TypeScript-проектов
Попробовать самому:
P.S. Если статья была полезна — звезда в репозитории и ваш фидбек в Issues помогут двигать проект дальше.
ссылка на оригинал статьи https://habr.com/ru/articles/943732/
Добавить комментарий