10. Обработка ошибок
11. Обработка ошибок в RxJS 🚨
Заголовок раздела «11. Обработка ошибок в RxJS 🚨»Привет! Яша снова здесь. Сегодня разбираем одну из самых важных тем — что делать, когда что-то пошло не так. В RxJS ошибки — это первоклассные граждане потока, и у нас есть целый арсенал инструментов для работы с ними.
Как ошибки распространяются в потоке
Заголовок раздела «Как ошибки распространяются в потоке»Когда в Observable возникает ошибка, поток завершается. Ошибка «падает вниз» по цепочке операторов до первого обработчика.
import { throwError, of } from 'rxjs';import { map } from 'rxjs/operators';
// Ошибка в операторе завершает потокof(1, 2, 3).pipe( map(n => { if (n === 2) throw new Error('Число 2 — плохое!'); return n; })).subscribe({ next: n => console.log('next:', n), // 1 error: e => console.log('error:', e.message), // Число 2 — плохое! complete: () => console.log('done'), // НЕ вызовется});💡 Важно: ошибка, брошенная внутри оператора, обрабатывается цепочкой. Ошибка, брошенная вне — например, синхронно в subscribe — падает в глобальный обработчик и НЕ перехватывается
catchError.
throwError — создать поток-ошибку
Заголовок раздела «throwError — создать поток-ошибку»throwError создаёт Observable, который немедленно бросает ошибку и завершается.
import { throwError } from 'rxjs';
// Фабрика ошибки (рекомендуемый способ в RxJS 7+)const error$ = throwError(() => new Error('Что-то сломалось'));
error$.subscribe({ next: v => console.log(v), // не вызовется error: e => console.log(e.message), // Что-то сломалось});EMPTY — поток, который молча завершается
Заголовок раздела «EMPTY — поток, который молча завершается»EMPTY — Observable, который сразу завершается без значений и без ошибок. Удобен как «заглушка» при обработке ошибок.
import { EMPTY, of } from 'rxjs';import { switchMap } from 'rxjs/operators';
// Если id нет — не делаем ничегоconst fetchUser = (id: string | null) => id ? of({ name: 'Яша', id }) : EMPTY;
fetchUser(null).subscribe({ next: u => console.log(u), complete: () => console.log('завершено без значений'),});catchError — перехват и восстановление
Заголовок раздела «catchError — перехват и восстановление»catchError перехватывает ошибку и позволяет вернуть другой Observable — продолжить поток или завершить его.
import { of, throwError } from 'rxjs';import { catchError, map } from 'rxjs/operators';
const api$ = throwError(() => new Error('500 Internal Server Error'));
api$.pipe( catchError(err => { console.log('Поймали ошибку:', err.message); return of({ data: null, error: err.message }); // возвращаем fallback })).subscribe(console.log);// { data: null, error: '500 Internal Server Error' }catchError получает два аргумента: (error, caught$). Второй — исходный Observable, что позволяет переподписаться:
import { of, throwError, timer } from 'rxjs';import { catchError, switchMap } from 'rxjs/operators';
// Переподписка с задержкой — ручной retrylet attempt = 0;const flaky$ = new Observable(obs => { attempt++; if (attempt < 3) obs.error(new Error(`Попытка ${attempt} неудачна`)); else { obs.next('Успех!'); obs.complete(); }});
flaky$.pipe( catchError((err, caught$) => { console.log(err.message); return timer(1000).pipe(switchMap(() => caught$)); })).subscribe(console.log); // Через 2 секунды: Успех!retry — автоматические повторные попытки
Заголовок раздела «retry — автоматические повторные попытки»retry(n) автоматически переподписывается на источник при ошибке, до n раз.
import { throwError, of } from 'rxjs';import { retry, map } from 'rxjs/operators';
let calls = 0;const flakyApi$ = new Observable<string>(obs => { calls++; console.log(`Запрос #${calls}`); if (calls < 3) obs.error(new Error('Сеть упала')); else { obs.next('Данные получены ✅'); obs.complete(); }});
flakyApi$.pipe( retry(3) // попробуем ещё 3 раза при ошибке).subscribe({ next: v => console.log(v), error: e => console.log('Всё, сдаёмся:', e.message),});// Запрос #1, Запрос #2, Запрос #3 → Данные получены ✅В RxJS 7+ retry принимает конфиг-объект:
import { retry } from 'rxjs/operators';
flakyApi$.pipe( retry({ count: 3, delay: 1000, // ждать 1 секунду между попытками resetOnSuccess: true, // сбросить счётчик после успеха })).subscribe(console.log);retryWhen — умные повторные попытки (RxJS 6)
Заголовок раздела «retryWhen — умные повторные попытки (RxJS 6)»retryWhen даёт полный контроль: принимает Observable ошибок и позволяет задать любую логику задержки.
import { retryWhen, delay, take, tap } from 'rxjs/operators';import { throwError, timer } from 'rxjs';
flakyApi$.pipe( retryWhen(errors$ => errors$.pipe( tap((err, i) => console.log(`Ошибка ${i + 1}: ${err.message}`)), delay(2000), // экспоненциальная задержка вручную take(3), // максимум 3 повтора ) )).subscribe(console.log);⚠️
retryWhenустарел в RxJS 7. Используйтеretry({ delay: ... })илиretry({ delay: (err, count) => timer(count * 1000) }).
onErrorResumeNextWith — игнорировать ошибки и продолжать
Заголовок раздела «onErrorResumeNextWith — игнорировать ошибки и продолжать»onErrorResumeNextWith (ранее onErrorResumeNext) переключается на следующий Observable при ошибке без передачи ошибки подписчику.
import { onErrorResumeNextWith } from 'rxjs/operators';import { throwError, of } from 'rxjs';
throwError(() => new Error('Провал!')).pipe( onErrorResumeNextWith(of('резервный источник'), of('и ещё один'))).subscribe(console.log);// резервный источник// и ещё одинСравнительная таблица операторов ошибок
Заголовок раздела «Сравнительная таблица операторов ошибок»| Оператор | Что делает при ошибке | Передаёт ошибку? |
|---|---|---|
catchError | Переключается на другой Observable | Нет (если вернуть Observable) |
retry(n) | Переподписывается N раз | Да (если все попытки исчерпаны) |
retryWhen | Переподписывается по условию | Да (если источник завершился) |
onErrorResumeNextWith | Переходит к следующему Observable | Нет |
EMPTY | Тихое завершение | Нет |
Практика
Заголовок раздела «Практика»Попробуйте примеры в интерактивном редакторе: