Этот мем смешной, пока не осознаешь, что в реальных проектах мы именно так и поступаем. Только заворачиваем не весь код сразу, а каждый 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 помогут двигать проект дальше.
Автор: Asouei
