🇺🇸 EN
Angular
·7 min read·↗·🇺🇸 EN

Cómo manejar y capturar errores en RxJS y Angular

Portada principal del artículo: Cómo manejar y capturar errores en RxJS y Angular

Cuando trabajamos con observables de RxJS en Angular, el manejo de errores es uno de los primeros obstáculos con los que nos encontramos. Es muy común intentar usar un bloque try-catch tradicional de JavaScript, solo para descubrir que no captura absolutamente nada cuando ocurre un error asíncrono en una petición HTTP o en un flujo de datos.

⚡ ¿Usas Angular moderno (v17+)? Salta directamente a Angular moderno: Signals, inject() y manejo limpio de errores para ver cómo manejar errores limpiamente sin suscripciones manuales ni fugas de memoria.

En este artículo te voy a mostrar por qué try-catch no funciona con flujos asíncronos, cómo usar los operadores clave de RxJS (catchError, throwError, EMPTY, retry) y cómo gestionar errores de forma reactiva en Angular moderno usando Signals y toSignal.

Empecemos por entender el problema usando un servicio de API típico.


1. El escenario: un error de API no controlado 🍺

Imagina un servicio de Angular que obtiene una lista de productos o cervezas desde una API:

// Servicio tradicional de Angular (v2 a v16)
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
 
@Injectable({ providedIn: 'root' })
export class BeerService {
  private apiUrl = 'https://api.example.com/beers';
  constructor(private http: HttpClient) {}
 
  getBeers(): Observable<any[]> {
    return this.http.get<any[]>(this.apiUrl);
  }
}

Ahora, veamos un componente que intenta proteger esta llamada con un bloque try-catch estándar:

export class AppComponent implements OnInit {
  beers: any[] = [];
  errorMessage = '';
 
  constructor(private beerService: BeerService) {}
 
  ngOnInit() {
    try {
      this.beerService.getBeers().subscribe((data) => {
        this.beers = data;
      });
    } catch (err) {
      // ❌ ¡ESTO NUNCA SE EJECUTARÁ cuando falle la petición HTTP!
      this.errorMessage = 'An error occurred';
    }
  }
}

Por qué try-catch falla con los observables

Un bloque try-catch es síncrono. Se ejecuta durante la configuración de la suscripción y termina de inmediato. Cuando la petición HTTP falla segundos después, esa ejecución ocurre dentro de la cola de callbacks asíncronos, totalmente fuera del alcance del try-catch original.

Ahora veamos dónde termina realmente el error dentro de una suscripción.


2. Capturar errores en el objeto suscriptor 📬

Al llamar a .subscribe(), el objeto suscriptor acepta tres callbacks: next, error y complete:

this.beerService.getBeers().subscribe({
  next: (data) => {
    this.beers = data;
  },
  error: (err) => {
    console.error('HTTP Request failed:', err);
    this.errorMessage = 'Could not load items from the server.';
  },
  complete: () => {
    console.log('Stream completed successfully.');
  }
});

Manejar los errores dentro de error: funciona para casos simples, pero rompe la composición del flujo cuando necesitas encadenar operadores.

Veamos cómo manejar errores dentro de la tubería del flujo usando operadores de RxJS.


3. Operadores de RxJS para el manejo de errores 🛠️

RxJS cuenta con operadores dedicados para interceptar errores antes de que rompan la lógica de tu aplicación.

1. catchError: proporcionar valores de respaldo

catchError intercepta un error en el flujo y te permite retornar un observable de respaldo (usando of()) para que el componente reciba un estado por defecto válido en lugar de romperse:

import { catchError, of } from 'rxjs';
 
this.beerService.getBeers().pipe(
  catchError((error) => {
    console.warn('API error caught, providing cached fallback:', error);
    return of([{ id: 0, name: 'Default Offline Beer' }]);
  })
).subscribe((beers) => {
  this.beers = beers; // ¡Recibe el array de respaldo limpiamente!
});

2. throwError: transformar y relanzar el error

Si necesitas registrar o transformar el error en un mensaje claro para el usuario y dejar que el suscriptor lo maneje:

import { catchError, throwError } from 'rxjs';
 
this.beerService.getBeers().pipe(
  catchError((error) => {
    const customMessage = error.status === 404 
      ? 'The requested beer catalog was not found.' 
      : 'Internal server error. Please try again later.';
    return throwError(() => new Error(customMessage));
  })
).subscribe({
  next: (data) => this.beers = data,
  error: (err) => this.errorMessage = err.message
});

3. EMPTY: completar el flujo silenciosamente

Si ocurre un error y prefieres evitar que las emisiones continúen sin detonar un estado de fallo, puedes retornar EMPTY:

import { catchError, EMPTY } from 'rxjs';
 
this.beerService.getBeers().pipe(
  catchError((error) => {
    console.error('Silent failure:', error);
    return EMPTY; // El flujo se completa de inmediato con 0 emisiones
  })
).subscribe((beers) => {
  this.beers = beers;
});

4. retry: reintentar ante fallos temporales

Para problemas temporales de red o caídas breves del servidor, coloca retry antes de catchError:

import { retry, catchError, of } from 'rxjs';
 
this.beerService.getBeers().pipe(
  retry({ count: 2, delay: 1000 }), // Reintenta hasta 2 veces con 1s de espera
  catchError((error) => of([]))
).subscribe(data => this.beers = data);

Ahora veamos cómo Angular moderno (v17+) simplifica todo esto con Signals y toSignal.


4. Angular moderno: Signals, inject() y manejo limpio de errores ⚡

En Angular moderno (v17+), evitamos hacer llamadas manuales a .subscribe() en los componentes. En su lugar, usamos Componentes Standalone, la función inject() y la utilidad de interoperabilidad toSignal().

Patrón de servicio moderno

// beer.service.ts (Standalone moderno)
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, of } from 'rxjs';
 
export interface Beer {
  id: number;
  name: string;
}
 
@Injectable({ providedIn: 'root' })
export class BeerService {
  private readonly http = inject(HttpClient);
  private readonly apiUrl = 'https://api.example.com/beers';
 
  getBeers() {
    return this.http.get<Beer[]>(this.apiUrl).pipe(
      catchError((error) => {
        console.error('BeerService failed:', error);
        return of([] as Beer[]); // Respaldo vacío seguro
      })
    );
  }
}

Patrón de componente moderno (sin suscripciones manuales)

// app.component.ts (Angular moderno v17+)
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { BeerService, Beer } from './beer.service';
 
@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <main class="p-6 max-w-2xl mx-auto">
      <h1 class="text-2xl font-bold mb-4">Beer Catalog</h1>
 
      @if (beers().length > 0) {
        <ul class="space-y-2">
          @for (beer of beers(); track beer.id) {
            <li class="p-3 bg-neutral-100 dark:bg-neutral-800 rounded-lg">
              {{ beer.name }}
            </li>
          }
        </ul>
      } @else {
        <p class="text-neutral-500">No beers available or server is offline.</p>
      }
    </main>
  `
})
export class AppComponent {
  private readonly beerService = inject(BeerService);
 
  // ¡Se convierte automáticamente en una Signal reactiva con valor inicial seguro!
  readonly beers = toSignal(this.beerService.getBeers(), { initialValue: [] as Beer[] });
}

Por qué el enfoque moderno es superior:

  1. Cero fugas de memoria: toSignal() se desuscribe automáticamente cuando el componente se destruye.
  2. Menos código repetitivo: Sin ngOnInit, sin ngOnDestroy y sin lógica manual de .subscribe().
  3. Reactividad nativa en plantillas: Funciona directamente con el nuevo flujo de control @if y @for.

Para tener claro cuándo conviene usar cada operador, veamos una tabla comparativa directa.


Tabla comparativa: estrategias de manejo de errores en RxJS 📊

Operador o enfoque¿Emite al suscriptor?¿Completa el flujo?Mejor caso de uso
catchError + of(respaldo)✅ Sí (next)✅ SíEntregar datos seguros por defecto o desde caché
catchError + throwError❌ No (error)❌ TerminadoRegistrar, personalizar y propagar errores
catchError + EMPTY❌ No✅ SíFallos silenciosos donde no se debe renderizar nada
retry({ count, delay })🔄 ReintentaSolo si todos los intentos fallanErrores transitorios de red o timeouts del servidor
toSignal() + catchError✅ Estado en SignalGestionado de forma nativaEstado reactivo basado en Signals en Angular moderno

Resumen 🛠️

Manejar errores en RxJS y Angular es directo y predecible cuando usas las herramientas adecuadas:

  1. Nunca uses un try-catch síncrono para observables asíncronos.
  2. Usa catchError con of() para suministrar datos de respaldo y mantener tu aplicación estable.
  3. Usa retry() antes de catchError para recuperarte automáticamente de fallos temporales en peticiones HTTP.
  4. En Angular moderno, combina inject(), catchError y toSignal() para obtener datos de forma declarativa y sin riesgo de fugas de memoria.

Si quieres conocer más patrones de arquitectura en Angular moderno, revisa mis artículos sobre Cómo usar ng-template, ng-container y ng-content y Cómo usar parámetros de ruta en Angular con Signals.

Parte de la serie Angular

These are my experiences learning and facing my daily challenges working with Angular.

Ver serie completa

Preguntas Frecuentes

¿Por qué try-catch no funciona para manejar errores en observables de RxJS?

Un bloque try-catch es síncrono y solo captura errores lanzados de inmediato en la pila de llamadas actual. Los observables de RxJS emiten valores de forma asíncrona a lo largo del tiempo, por lo que los errores en un flujo no llegan al try-catch que los envuelve. En su lugar, necesitas usar operadores de manejo de errores de RxJS como catchError dentro del pipe.

¿Cuál es la diferencia entre catchError, throwError y EMPTY en RxJS?

catchError intercepta un error en el flujo y retorna un observable de reemplazo (como un valor alternativo con of()). throwError relanza o transforma el error en el flujo para que lo manejen los suscriptores posteriores. EMPTY completa el flujo de inmediato sin emitir valores ni notificaciones de error.

¿Cómo manejo errores HTTP de forma reactiva en Angular moderno con Signals?

En Angular moderno, puedes usar la función toSignal() de @angular/core/rxjs-interop combinada con catchError en el pipe de tu servicio. Esto transforma tu flujo asíncrono en una Signal reactiva con un valor inicial seguro, eliminando las suscripciones manuales.

¿Cómo puedo reintentar automáticamente una petición HTTP fallida antes de capturar el error?

Puedes colocar el operador retry(count) antes de catchError en el pipe de tu observable. RxJS se volverá a suscribir al observable de origen hasta el número de veces indicado antes de enviar el error a catchError.

Artículos Relacionados

Compartir este artículo

Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.


Software real. Lecciones reales.

Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.

User avatar
User avatar
User avatar
User avatar
+13K

Únete a 13,800+ desarrolladores y lectores.

Cero spam. Cancela tu suscripción en cualquier momento.