Хватит писать try/catch вокруг fetch: история о том, как я устал ловить ошибки

от автора

"You can't have errors in your code if you wrap the entire codebase in a try/catch block"

«You can’t have errors in your code if you wrap the entire codebase in a try/catch block»

Этот мем смешной, пока не осознаешь, что в реальных проектах мы именно так и поступаем. Только заворачиваем не весь код сразу, а каждый 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); } 
Мем про обработку ошибок: exception в виде медведя, а try, catch и finally убегают от него

Мем про обработку ошибок: exception в виде медведя, а try, catch и finally убегают от него

Через месяц в проекте половина функций выглядит именно так. А проблемы одни и те же:

  • 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *