Хватит писать try/catch вокруг fetch: история о том, как я устал ловить ошибки
- пятница, 5 сентября 2025 г. в 00:00:10
Этот мем смешной, пока не осознаешь, что в реальных проектах мы именно так и поступаем. Только заворачиваем не весь код сразу, а каждый 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
}
});
Если сервер вернул 429 с заголовком - библиотека сама подождет:
// Сервер: 429 Too Many Requests, Retry-After: 60
// safe-fetch: ждем ровно 60 секунд
Можно подключить 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 помогут двигать проект дальше.