Observable, RxJS и обработка ошибок
Observable — это поток значений во времени, а RxJS — набор инструментов, чтобы этот поток фильтровать, преобразовывать и чинить при сбое.
«Promise — это одно письмо. Observable — это подписка на журнал: значения приходят по мере событий, и поток можно гибко обрабатывать».
HttpClient возвращает Observable из библиотеки RxJS. В отличие от промиса, который отдаёт одно значение, Observable — это поток: он может выдавать значения со временем, его можно отменять и трансформировать цепочкой операторов. Для HTTP это особенно удобно при обработке ответа и ошибок.
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, map, of } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ProductApi {
private http = inject(HttpClient);
getTitles() {
return this.http.get<{ title: string }[]>('/api/products').pipe(
map(list => list.map(p => p.title)), // преобразуем ответ
catchError(err => {
console.error('Ошибка загрузки', err);
return of([]); // подставляем запасное значение
}),
);
}
}
Метод pipe() пропускает поток через цепочку операторов. map преобразует данные, catchError ловит сбой и позволяет вернуть запасной поток (через of), чтобы приложение не падало. В компоненте поток удобнее всего превратить в сигнал:
import { toSignal } from '@angular/core/rxjs-interop';
export class ProductListComponent {
private api = inject(ProductApi);
titles = toSignal(this.api.getTitles(), { initialValue: [] as string[] });
}
Как работает под капотом
toSignal() подписывается на Observable за вас, кладёт каждое новое значение в сигнал и сам отписывается при уничтожении компонента — это снимает классическую проблему утечек памяти. Операторы в pipe() применяются по очереди: значение проходит сквозь map, а при ошибке поток «сворачивает» в catchError.
http.get() -> поток данных
|
.pipe(
map : [{title}] -> ['title1','title2']
catchError: ошибка -> of([]) (запасной поток)
)
|
toSignal() -> подписка + автоотписка -> titles() в шаблоне
Запускаемая врезка: мини-Observable (поток-наблюдатель)
Соберём упрощённый Observable с операторами. «Попробуй сам ▶».
// поток значений с операторами map и catchError
function fromValues(values) {
return {
subscribe(onNext) { values.forEach(onNext); },
pipe(...ops) { return ops.reduce((src, op) => op(src), this); },
};
}
const map = (fn) => (src) => ({
subscribe: (onNext) => src.subscribe(v => onNext(fn(v))),
pipe: fromValues([]).pipe,
});
const log = [];
fromValues([{title:'Кофемолка'}, {title:'Чайник'}])
.pipe(map(p => p.title.toUpperCase()))
.subscribe(v => log.push(v));
console.log(log); // ["КОФЕМОЛКА", "ЧАЙНИК"]
Частые ошибки
- Ручная подписка без отписки. Это утечка памяти;
toSignalили async-pipe решают проблему. - Глотать ошибку молча.
catchErrorдолжен либо вернуть запасное значение, либо пробросить осмысленную ошибку. - Городить вложенные подписки. Для зависимых запросов есть операторы вроде
switchMap.
Best practices
- Преобразуйте данные операторами (
map), а не в компоненте. - Всегда обрабатывайте сбои через
catchErrorс запасным сценарием. - Переводите потоки в сигналы через
toSignal()— это убирает ручные подписки.
Итоги. Observable — поток значений, RxJS — операторы над ним (map, catchError), а toSignal() связывает потоки с сигнальной реактивностью без утечек. Дальше — финальная сборка и деплой.
Закрепляем
Observable из библиотеки RxJS — это поток значений во времени, и его удобно противопоставлять промису. Промис похож на одно письмо: один результат, и всё. Observable — это подписка на журнал: значения могут приходить по мере событий, поток можно отменять, фильтровать и преобразовывать цепочкой операторов. Для HTTP это особенно ценно при обработке ответа и, главное, ошибок.
Запомните рабочий минимум операторов. map преобразует данные потока, catchError ловит сбой и позволяет вернуть запасной поток, чтобы приложение не рухнуло из-за одной неудачной загрузки. Применяются они внутри pipe(), проходя значение по очереди. А чтобы не возиться с ручными подписками и не плодить утечки памяти, оборачивайте поток в toSignal(): он подпишется за вас, будет класть новые значения в сигнал и сам отпишется при уничтожении компонента. Преобразуйте данные операторами, а не в компоненте, и всегда обрабатывайте ошибки осмысленно — это признак зрелого кода.
| Оператор / инструмент | Назначение |
|---|---|
| map | Преобразовать значения |
| catchError | Перехватить ошибку |
| of(value) | Запасной поток |
| toSignal() | Подписка без утечек |