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()Подписка без утечек
Проверьте себя
1. Чем Observable отличается от Promise?
AНичем
BObservable — поток, который может выдавать много значений во времени, его можно отменять и трансформировать операторами; Promise отдаёт одно значение
CObservable быстрее
DPromise работает только в Angular
2. Зачем оборачивать Observable в toSignal()?
AЧтобы ускорить сеть
BОн подписывается за вас, кладёт значения в сигнал и автоматически отписывается при уничтожении компонента (нет утечек)
CЧтобы отменить запрос
DЭто обязательно для любого Observable