CodeForge

Angular 21 de Cero a Experto / RxJS y observables a gran escala

Errores y reintentos en RxJS

Teoría20 min20 XP

Los streams reales fallan: una petición se cae, la red parpadea, el servidor devuelve un 500. En RxJS, un error TERMINA el Observable —el grifo se cierra— a menos que lo manejes. Hoy aprendes a atrapar errores con catchError, a reintentar con retry, y a mantener tu stream vivo pese a los fallos. La diferencia entre una app que se rompe con el primer error de red y una que se recupera con elegancia.

El fusible que salta

Un error termina el stream

error-termina.ts
// si la petición falla, el Observable emite 'error' y COMPLETA (muere)
this.buscador.valueChanges.pipe(
switchMap((q) => this.api.buscar(q)),   // si esto falla…
).subscribe({
next: (r) => this.mostrar(r),
error: (e) => console.error('el stream murió', e),   // …llega aquí y NO vuelve a emitir
});
// tras el error, escribir en el buscador ya NO dispara búsquedas: el stream terminó

Este es el error conceptual número uno con RxJS: un fallo dentro del pipe mata TODO el stream, no solo esa emisión. Por eso el manejo de errores va DENTRO del pipe, cerca de donde puede fallar.

catchError: atrapar y reemplazar

catchError intercepta el error y devuelve un Observable de reemplazo, evitando que el stream muera:

catcherror.ts
import { catchError, of } from 'rxjs';

this.buscador.valueChanges.pipe(
switchMap((q) =>
  this.api.buscar(q).pipe(
    catchError((err) => {
      console.error(err);
      return of([]);   // reemplaza el error por una lista vacía → el stream sigue vivo
    }),
  ),
),
).subscribe((r) => this.mostrar(r));

retry: intentar de nuevo

Para fallos transitorios (red inestable), retry vuelve a suscribirse al Observable fallido, reintentando la operación:

retry.ts
import { retry, timer } from 'rxjs';

this.api.cargarGastos().pipe(
// reintenta hasta 3 veces, con espera creciente entre intentos (backoff)
retry({
  count: 3,
  delay: (error, intento) => timer(intento * 1000),   // 1s, 2s, 3s
}),
catchError(() => of([])),   // si tras 3 intentos sigue fallando, valor de reemplazo
).subscribe((gastos) => this.gastos.set(gastos));

El delay con backoff (espera creciente) es la buena práctica: no bombardees un servidor que ya está en problemas; dale tiempo entre reintentos. Y siempre combina retry con un catchError final, por si todos los reintentos fallan.

Pruébalo: la lógica de reintento con backoff

La política de reintentos —cuántas veces, cuánto esperar, cuándo rendirse— es lógica pura. Aquí, un reintento con backoff simulando una operación inestable, en vivo:

Observa el patrón: intenta, y si falla, espera un tiempo CRECIENTE antes de reintentar, hasta un máximo. Si se agotan los intentos, propaga el error final (donde catchError pondría el valor de reemplazo). Es exactamente lo que hace retry({ count, delay }) de RxJS —lo has construido con async/await para ver la mecánica—.

¿Por qué se coloca `catchError` DENTRO del `switchMap` (envolviendo la petición) y no en el pipe externo?

Mini-reto

En el playground: 1) cambia operacionInestable para que falle SIEMPRE y verifica que tras agotar los intentos se lanza el error final; 2) añade un backoff exponencial (2 ** intento * baseMs en vez de lineal) y observa las esperas; 3) piensa cómo escribirías esto en RxJS con retry({ count, delay }) + catchError(() => of(valorPorDefecto)).

Qué sigue

Dominas RxJS: streams, operadores, aplanamiento, combinación y errores. Ahora la pregunta que define Angular moderno: si ya tienes señales para el estado, ¿cuándo usas RxJS y cuándo señales? Y ¿cómo conectas ambos mundos? La próxima lección es la interop toSignal/toObservable —el puente entre los dos sistemas reactivos— y la guía definitiva de cuándo usar cada uno.