-
Второй шаг в мир RxJS: Операторы RxJS — как изучать и зачем они нужны
-
Четвертый шаг в мир RxJs: незавершенные потоки — тихие убийцы приложений
Вы уже встречались с этими «веселыми» историями, когда разработчик заканчивает работу над задачей, она проходит тестирование, отправляется в прод, а там встречается неожиданным отказом какого-нибудь мелкого метода api и укладывает всё приложение так, что пользователи наблюдают только белый экран?
Я в своё время познакомился с ними чересчур близко… И, честно сказать, потоки RxJs прекрасные учителя — тебе не захочется снова повторять их уроки. Чему же они нас учат? В первую очередь тому, что не стоит доверять внешним источникам; вы не контролируете ни соединение с сервером, ни api-сервис, а значит не имеете никаких оснований слепо доверять им и ожидать безотказной работы.
Если ваш бэк имеет коэффициент доступности в пять девяток (отличный результат!), он по-прежнему не работает несколько минут в году. Отказы бывают у любых систем.
Типичные сценарии новичков
Призрачные данные
this.api.getData().subscribe( data => this.render(data) // А если data === undefined? );
Немой сбой
combineLatest( loadUsers(), loadProducts() // Если упадёт здесь — всё остановится ).subscribe();
Эффект домино
interval(1000).pipe( switchMap(() => new Observable(o => { o.error('Error!') })) ).subscribe(); // При ошибке падает весь поток, данные перестают обновляться
«Хороший разработчик пишет код. Отличный — предвидит, как он сломается»
— Неизвестный Архитектор*
Базовые операторы для работы с ошибками: ваш набор «скорой помощи» (актуально для RxJS 7.8+)
catchError: цифровая аптечка
Как это работает
Представьте, что ваш Observable — это курьерская служба. catchError — это страховая компания, да, она не сможет вернуть потерянный при доставке товар, но попробует предложить замену (денежная компенсация вас устроит?).
Практика
import {catchError} from 'rxjs/operators'; import {of} from 'rxjs'; const request = new Observable(o => { o.error('Error!'); o.complete(); }); request.pipe( catchError(error => { console.log(error); // Логируем проблему return of([]); // Возвращаем пустой массив как fallback }) ).subscribe(orders => { console.log(orders); // Всегда получим данные });
Здесь и далее в примерах я буду приводить именно такую форму, с самописной
Observable. Давайте договоримся, что в реальности там будет что-то в духеthis.http.get('/api/orders'); для примера эта запись не подходит, потому что её придется переписывать для собственных экспериментов, а сObservableможно скопировать код и проводить опыты. Так же вместо записей в духеthis.logService.report(error)я оставляюconsole.log(error), по той же причине.
retry: умный повтор с контролем
Философия
Как хороший бариста делает кофе заново при ошибке — так retry повторяет запросы. Но помните: не все операции идемпотентны!
Пример с конфигурацией
import {retry, timer} from 'rxjs'; const request = new Observable(o => { o.error('Error!'); o.complete(); }); request.pipe( retry({ count: 3, // Максимум 3 попытки delay: (error, retryCount) => timer(1000 * retryCount) // Задержка растёт: 1s, 2s, 3s }) ).subscribe();
Правила безопасности
-
Никогда не используйте для POST/PUT-запросов
-
Всегда устанавливайте разумный лимит попыток
-
Комбинируйте с задержками для защиты сервера
finalize: гарантированная уборка
Почему это важно
Война войной, а обед по расписанию, finalize выполнится при любом исходе:
-
Успешное завершение
-
Ошибка
-
Ручная отписка
Идеальный кейс
this.loading = true; const request = new Observable(o => { o.error('Error!'); o.complete(); }); request.pipe( finalize(() => { this.loading = false; // Всегда сбрасываем флаг console.log('DataLoadCompleted'); }) ).subscribe();
Почему мы больше не используем retryWhen?
-
Устаревший подход
retryWhenобъявлен deprecated в RxJS 7.8+ -
Новые возможности
Объект конфигурацииretryпроще и безопаснее:
retry({ count: 4, delay: (_, i) => timer(1000 * 2 ** i) // Экспоненциальная задержка })
3. Читаемость кода
Конфиг-объект делает логику повторов явной
Советы из боевого опыта
-
Правило трёх уровней
-
Уровень 1: Повтор запроса (
retry) -
Уровень 2: Fallback-данные (
catchError) -
Уровень 3: Глобальный обработчик
-
-
Правило 80/20
80% ошибок обрабатывайте черезcatchError, 20% — через сложные стратегии. -
Логирование — как дневник
Всегда записывайте:-
Тип ошибки
-
Контекст операции
-
Временную метку
-
-
Тестируйте failure-сценарии
На каждый десяток позитивных тестов добавляйте 1-2 теста с ошибками.
«Код без обработки ошибок — как дом без пожарного выхода: работает, пока не случится беда»
Примеры использования операторов: от теории к практике
Сценарий 1: Грациозная деградация данных
Проблема
Приложение падает, если API возвращает 404 на странице товара.
Решение с catchError
this.product$ = this.http.get(`/api/products/${id}`).pipe( catchError(error => { if (error.status === 404) { return of({ id, name: 'Товар временно недоступен', image: '/assets/placeholder.jpg' }); } throw error; // Пробрасываем другие ошибки }) );
Эффект:
Пользователь видит информативную карточку вместо белого экрана.
Сценарий 2: Умный повтор запросов
Проблема
Мобильные клиенты часто теряют связь при загрузке ленты новостей.
Решение с retry
this.http.get('/api/feed').pipe( retry({ count: 3, // Максимум 3 попытки delay: (error, retryCount) => timer(1000 * retryCount) // Линейная задержка }), catchError(err => { this.offlineService.showWarning(); return EMPTY; }) ).subscribe();
Статистика:
Уменьшение ошибок загрузки в условиях нестабильной сети.
Сценарий 3: Комплексная обработка платежей
Проблема
Нужно гарантировать выполнение клиринга даже при ошибках.
Комбинация операторов
processPayment(paymentData).pipe( retry(2), // Повтор для временных сбоев catchError(error => { this.fallbackProcessor.process(paymentData); return EMPTY; }), finalize(() => { this.cleanupResources(); this.logService.flush(); // Гарантированная запись логов }) ).subscribe();
Архитектурный совет:
Всегда разделяйте «повторяемые» и «фатальные» ошибки.
Сценарий 4: Фоновые синхронизации
Проблема
Фоновый процесс синхронизации «зависает» при ошибках.
Решение с finalize
this.syncJob = interval(30_000).pipe( switchMap(() => this.syncService.run()), finalize(() => { this.jobRegistry.unregister('background-sync'); this.memoryCache.clear(); }) ).subscribe();
Важно:
Даже при ручной отписке ресурсы будут освобождены.
Советы из боевых условий
Паттерн «Слоёная защита»
Комбинируйте операторы как фильтры:
Поток → retry(3) → catchError → finalize
Метрики — ваши друзья
Добавляйте счётчики ошибок:
catchError(error => { this.metrics.increment('API_ERRORS'); throw error; })
Тестируйте крайние случаи
Используйте marble-диаграммы для моделирования ошибок:
cold('--a--#', null, new Error('Timeout')).pipe(...)
Стратегии обработки ошибок: как не закопаться в исключениях
1. Стратегия «Острова безопасности»
Концепция
Разбивайте поток на независимые сегменты с локальной обработкой ошибок. Как водонепроницаемые отсеки в корабле.
Реализация
merge( this.loadUserData().pipe( catchError(() => of(null)) // Ошибка не сломает другие потоки ), this.loadProducts().pipe( retry(2) // Своя политика повторов ) ).pipe( finalize(() => this.hideLoader()) // Общая точка очистки );
Эффект:
Ошибка в одном потоке не останавливает работу всей системы.
2. Стратегия «Эшелонированная защита»
Трехуровневая модель
Уровень запроса:
Повторы для временных сбоев
this.http.get(...).pipe(retry(3))
Уровень компонента:
Fallback-данные
catchError(() => this.cache.getData())
Уровень приложения:
Глобальный перехватчик ошибок
@Injectable() export class GlobalErrorHandler implements ErrorHandler { handleError(error) { this.sentry.captureException(error); } }
3. Стратегия «Умного повтора»
Когда использовать
-
Сервисы с нестабильным соединением
-
Критически важные операции
-
Фоновые синхронизации
Шаблон «Экспоненциальный бекофф»
retryWhen(errors => errors.pipe( retry({ count: 4, delay: (error, retryCount) => timer(1000 * 2 ** i) }), catchError(err => this.fallbackStrategy()) ));
Статистика из практики:
Успешное восстановление в большинстве случаев при 4 попытках.
4. Стратегия «Тихий отказ»
Для чего
-
Не критичные к данным компоненты
-
Демо-режимы
-
Возможна деградация функционала (переход со стримов на http-запросы)
Реализация
this.liveUpdates$ = websocketStream.pipe( catchError(() => interval(5000).pipe( switchMap(() => this.http.get('/polling-endpoint')) ) ) );
Эффект:
Пользователь продолжает работу в ограниченном режиме.
5. Стратегия «Явного краха»
Когда нужно
-
Операции с деньгами
-
Юридически значимые действия
-
Системы безопасности
Реализация
processTransaction().pipe( tap({error: () => this.rollbackTransaction()}), catchError(error => { this.showFatalError(); throw error; }) );
Золотое правило:
Лучше явная ошибка, чем некорректное состояние.
Чек-лист выбора стратегии
-
Насколько критична операция?
-
Деньги/безопасность → «Явный крах»
-
Просмотр данных → «Тихий отказ»
-
-
Как часто возникают ошибки?
-
Часто → «Эшелонированная защита»
-
Редко → «Острова безопасности»
-
-
Какие ресурсы доступны?
-
Есть кеш → «Умный повтор»
-
Нет резервов → «Тихий отказ»
-
«Стратегия без метрик — как компас без стрелки»
— Принцип observability в микросервисах
Распространённые ошибки новичков: как не наступить на грабли
1. Молчаливое проглатывание ошибок
❌ Проблемный подход
this.http.get('/api/data').subscribe(data => { // Ошибки? Какие ошибки? this.render(data); });
Последствия:
Пользователь видит «зависший» интерфейс, ошибки не логируются.
✅ Правильное решение
this.http.get('/api/data').subscribe({ next: data => this.render(data), error: err => this.handleError(err) // Всегда обрабатываем ошибку });
2. Бесконечные повторы
❌ Опасный код
this.http.get('/api/orders').pipe( retry() // Бесконечный цикл при 500 ошибке );
Риски:
DDoS своего же сервера.
✅ Безопасный вариант
retry(3) // Чёткий лимит попыток
3. Игнорирование отписок
❌ Типичная ситуация
ngOnInit() { interval(1000).subscribe(data => { // При переходе на другой роут — подписка живёт вечно this.updateRealTimeData(data); }); }
Эффект:
Утечки памяти, конфликты обновлений.
✅ Профессиональный подход
let destroy$ = new Subject<void>(); ngOnInit() { interval(1000).pipe( takeUntil(this.destroy$) ).subscribe(...); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }
4. Глобальный перехватчик как «мусорка»
❌ Антипаттерн
// global-error-handler.ts handleError(error) { // Ловим ВСЕ ошибки без разбора this.sentry.captureException(error); }
Проблема:
Невозможно кастомизировать обработку для конкретных сценариев.
✅ Стратифицированный подход
// Локальная обработка catchError(err => handleLocalError(err)) // Глобальный перехватчик handleError(error) { if (error.isCritical) { this.sentry.captureException(error); } }
Чек-лист для самопроверки
-
Все ли подписки имеют обработку
error? -
Есть ли ограничения у
retry? -
Используется ли
takeUntilдля отписок? -
Разделены ли глобальные и локальные ошибки?
«Ошибки — как грабли: чтобы перестать наступать, нужно сначала увидеть их в коде»
— Очередной «ауф»-принцип из пацанских пабликов
Заключение: Ошибки как путь к мастерству
Что мы узнали?
За последние годы работы с RxJS я понял: настоящее мастерство начинается там, где другие видят проблемы. Обработка ошибок — не рутина, а искусство проектирования отказоустойчивых систем.
Главные уроки:
-
Обработка ошибок — индикатор зрелости кода
КаждыйcatchErrorв вашем коде — это шаг к профессиональному уровню. -
Повторы ≠ панацея
Правильно настроенный и продуманныйretryспасёт там, где базовая реализация навредит.
Куда двигаться дальше?
-
Экспериментируйте с комбинациями
Пример продвинутой цепочки:data$.pipe( retryWhen(exponentialBackoff(1000, 3)), catchError(switchToCache), finalize(cleanup) ) -
Изучайте реальные кейсы
Исходники Angular HttpClient и Ngrx — кладезь паттернов. -
Делитесь знаниями
Напишите пост о своей самой сложной ошибке — это лучший способ закрепить опыт.
«За 20 лет в разработке я видел много ‘идеальных’ систем. Все они ломались. Выживали те, где ошибки были частью дизайна.»
— Кто-то это, определенно, когда-то кому-то сказал*
Ваш следующий шаг:
Откройте свой последний проект. Найдите хотя бы один поток без обработки ошибок — и превратите его в пример надёжности. Помните: каждый обработанный сбой — это спасённые часы поддержки и тысячи довольных пользователей.
ссылка на оригинал статьи https://habr.com/ru/articles/910232/
Добавить комментарий