Manejo de errores

Intermedio
Angular
Angular
Actualizado: 25/09/2025

CatchError básico

Los errores en peticiones HTTP son inevitables en aplicaciones reales. La red puede fallar, el servidor puede estar inaccesible, o la API puede devolver errores específicos. El operador catchError de RxJS nos permite interceptar estos errores y manejarlos de forma elegante.

Importación y configuración

Para usar catchError con HttpClient, necesitamos importarlo desde RxJS:

import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { catchError } from 'rxjs/operators';
import { of, throwError } from 'rxjs';

Capturar errores básicos

El patrón más simple es usar catchError para interceptar cualquier error en una petición HTTP:

@Component({
  selector: 'app-productos',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div>
      @if (productos.length > 0) {
        @for (producto of productos; track producto.id) {
          <div>{{ producto.nombre }}</div>
        }
      } @else {
        <p>No se pudieron cargar los productos</p>
      }
    </div>
  `
})
export class ProductosComponent {
  productos: any[] = [];

  constructor(private http: HttpClient) {
    this.cargarProductos();
  }

  cargarProductos() {
    this.http.get<any[]>('/api/productos')
      .pipe(
        catchError(error => {
          console.error('Error al cargar productos:', error);
          return of([]); // Devuelve array vacío como fallback
        })
      )
      .subscribe(data => {
        this.productos = data;
      });
  }
}

Devolver valores por defecto

Una estrategia común es devolver un valor por defecto cuando ocurre un error. Esto evita que la aplicación se rompa completamente:

obtenerUsuario(id: number) {
  return this.http.get<Usuario>(`/api/usuarios/${id}`)
    .pipe(
      catchError(error => {
        console.error(`Error al obtener usuario ${id}:`, error);
        // Devuelve un usuario por defecto
        return of({
          id: 0,
          nombre: 'Usuario no encontrado',
          email: 'N/A'
        });
      })
    );
}

Diferenciar tipos de errores HTTP

Angular proporciona HttpErrorResponse que nos permite identificar diferentes tipos de errores:

obtenerDatos() {
  return this.http.get('/api/datos')
    .pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 0) {
          // Error de conectividad (sin internet, servidor caído)
          console.error('Error de conexión:', error.error);
          return of({ mensaje: 'Sin conexión a internet' });
        }

        if (error.status >= 400 && error.status < 500) {
          // Errores del cliente (4xx)
          console.error('Error del cliente:', error.status, error.message);
          return of({ mensaje: 'Solicitud inválida' });
        }

        if (error.status >= 500) {
          // Errores del servidor (5xx)
          console.error('Error del servidor:', error.status, error.message);
          return of({ mensaje: 'Error interno del servidor' });
        }

        // Error desconocido
        console.error('Error desconocido:', error);
        return of({ mensaje: 'Error inesperado' });
      })
    );
}

Propagar errores cuando sea necesario

En algunos casos, queremos manejar el error pero aún así propagarlo para que otros operadores o componentes puedan reaccionar:

guardarDatos(datos: any) {
  return this.http.post('/api/datos', datos)
    .pipe(
      catchError(error => {
        // Log del error para debugging
        console.error('Error al guardar:', error);
        
        // Determina si el error es recuperable
        if (error.status === 422) {
          // Error de validación - propaga el error
          return throwError(() => error);
        }
        
        // Error no recuperable - devuelve valor por defecto
        return of({ success: false, message: 'Error al guardar' });
      })
    );
}

Manejo específico por código de estado

Para APIs que devuelven códigos de estado específicos, podemos crear respuestas personalizadas:

buscarProducto(id: number) {
  return this.http.get<Producto>(`/api/productos/${id}`)
    .pipe(
      catchError((error: HttpErrorResponse) => {
        switch (error.status) {
          case 404:
            return of({ 
              id: 0, 
              nombre: 'Producto no encontrado',
              disponible: false 
            });
          
          case 401:
            // Usuario no autenticado
            console.error('Usuario no autorizado');
            return throwError(() => new Error('Acceso denegado'));
          
          case 403:
            // Usuario autenticado pero sin permisos
            return of({ 
              id: 0, 
              nombre: 'Sin permisos para ver este producto',
              disponible: false 
            });
          
          default:
            return of({ 
              id: 0, 
              nombre: 'Error al cargar producto',
              disponible: false 
            });
        }
      })
    );
}

Ejemplo práctico en un servicio

Un servicio completo que maneja errores de forma consistente:

@Injectable({
  providedIn: 'root'
})
export class ProductosService {
  private apiUrl = '/api/productos';

  constructor(private http: HttpClient) {}

  obtenerTodos() {
    return this.http.get<Producto[]>(this.apiUrl)
      .pipe(
        catchError(error => {
          console.error('Error al obtener productos:', error);
          return of([]); // Lista vacía como fallback
        })
      );
  }

  obtenerPorId(id: number) {
    return this.http.get<Producto>(`${this.apiUrl}/${id}`)
      .pipe(
        catchError((error: HttpErrorResponse) => {
          if (error.status === 404) {
            return of(null); // Producto no encontrado
          }
          console.error(`Error al obtener producto ${id}:`, error);
          return throwError(() => error);
        })
      );
  }

  crear(producto: Producto) {
    return this.http.post<Producto>(this.apiUrl, producto)
      .pipe(
        catchError(error => {
          console.error('Error al crear producto:', error);
          // En operaciones de escritura, es mejor propagar el error
          return throwError(() => error);
        })
      );
  }
}

El operador catchError es esencial para crear aplicaciones robustas que no se rompan ante errores de red o servidor. Siempre debe colocarse en el pipe después de la petición HTTP y antes de cualquier suscripción.

Mostrar errores al usuario

Una vez que hemos capturado los errores con catchError, es fundamental presentar esta información de manera clara y útil a los usuarios. Una buena experiencia de usuario requiere mensajes comprensibles y feedback visual adecuado.

Estados de error en componentes

La forma más efectiva de manejar errores en la UI es mediante estados reactivos que controlen qué se muestra al usuario:

@Component({
  selector: 'app-productos',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="contenedor-productos">
      @if (cargando) {
        <div class="loading">Cargando productos...</div>
      } @else if (mensajeError) {
        <div class="error">
          <h3>¡Ups! Algo salió mal</h3>
          <p>{{ mensajeError }}</p>
          <button (click)="reintentar()" class="btn-reintentar">
            Intentar de nuevo
          </button>
        </div>
      } @else {
        @for (producto of productos; track producto.id) {
          <div class="producto-card">{{ producto.nombre }}</div>
        }
      }
    </div>
  `
})
export class ProductosComponent {
  productos: any[] = [];
  cargando = false;
  mensajeError: string | null = null;

  constructor(private http: HttpClient) {
    this.cargarProductos();
  }

  cargarProductos() {
    this.cargando = true;
    this.mensajeError = null;

    this.http.get<any[]>('/api/productos')
      .pipe(
        catchError((error: HttpErrorResponse) => {
          this.mensajeError = this.obtenerMensajeError(error);
          return of([]);
        })
      )
      .subscribe(data => {
        this.productos = data;
        this.cargando = false;
      });
  }

  private obtenerMensajeError(error: HttpErrorResponse): string {
    if (error.status === 0) {
      return 'No se pudo conectar al servidor. Verifica tu conexión a internet.';
    }
    
    if (error.status === 404) {
      return 'Los productos no están disponibles en este momento.';
    }
    
    if (error.status >= 500) {
      return 'El servidor está experimentando problemas. Intenta de nuevo más tarde.';
    }
    
    return 'Ocurrió un error inesperado. Por favor, intenta de nuevo.';
  }

  reintentar() {
    this.cargarProductos();
  }
}

Mensajes específicos por contexto

Diferentes acciones requieren mensajes de error personalizados que ayuden al usuario a entender qué pasó y qué puede hacer:

@Component({
  selector: 'app-perfil-usuario',
  standalone: true,
  imports: [CommonModule, FormsModule],
  template: `
    <form (ngSubmit)="guardarPerfil()">
      <input [(ngModel)]="nombre" name="nombre" placeholder="Nombre">
      <input [(ngModel)]="email" name="email" placeholder="Email">
      
      @if (errorGuardado) {
        <div class="alert alert-error">
          <strong>Error al guardar:</strong>
          <p>{{ errorGuardado }}</p>
        </div>
      }
      
      @if (exitoGuardado) {
        <div class="alert alert-success">
          <p>¡Perfil actualizado correctamente!</p>
        </div>
      }
      
      <button type="submit" [disabled]="guardando">
        {{ guardando ? 'Guardando...' : 'Guardar cambios' }}
      </button>
    </form>
  `
})
export class PerfilUsuarioComponent {
  nombre = '';
  email = '';
  guardando = false;
  errorGuardado: string | null = null;
  exitoGuardado = false;

  constructor(private http: HttpClient) {}

  guardarPerfil() {
    this.guardando = true;
    this.errorGuardado = null;
    this.exitoGuardado = false;

    const datosUsuario = { nombre: this.nombre, email: this.email };

    this.http.put('/api/perfil', datosUsuario)
      .pipe(
        catchError((error: HttpErrorResponse) => {
          this.errorGuardado = this.procesarErrorGuardado(error);
          return throwError(() => error);
        })
      )
      .subscribe({
        next: () => {
          this.exitoGuardado = true;
          this.guardando = false;
          // Ocultar mensaje de éxito después de 3 segundos
          setTimeout(() => this.exitoGuardado = false, 3000);
        },
        error: () => {
          this.guardando = false;
        }
      });
  }

  private procesarErrorGuardado(error: HttpErrorResponse): string {
    if (error.status === 400) {
      return 'Los datos ingresados no son válidos. Revisa la información.';
    }
    
    if (error.status === 409) {
      return 'El email ya está en uso por otro usuario.';
    }
    
    if (error.status === 422) {
      // Error de validación del servidor
      const errores = error.error?.errors || {};
      if (errores.email) {
        return 'El formato del email no es válido.';
      }
      if (errores.nombre) {
        return 'El nombre es demasiado corto.';
      }
    }
    
    return 'No se pudieron guardar los cambios. Intenta de nuevo.';
  }
}

Alertas y notificaciones temporales

Para acciones puntuales como eliminaciones o actualizaciones, es útil mostrar notificaciones que desaparezcan automáticamente:

@Component({
  selector: 'app-lista-tareas',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="contenedor-tareas">
      @if (notificacion) {
        <div class="notificacion" [class]="notificacion.tipo">
          {{ notificacion.mensaje }}
        </div>
      }
      
      @for (tarea of tareas; track tarea.id) {
        <div class="tarea-item">
          <span>{{ tarea.titulo }}</span>
          <button 
            (click)="eliminarTarea(tarea.id)"
            [disabled]="eliminando === tarea.id"
          >
            {{ eliminando === tarea.id ? 'Eliminando...' : 'Eliminar' }}
          </button>
        </div>
      }
    </div>
  `
})
export class ListaTareasComponent {
  tareas: any[] = [];
  eliminando: number | null = null;
  notificacion: { mensaje: string; tipo: string } | null = null;

  constructor(private http: HttpClient) {
    this.cargarTareas();
  }

  eliminarTarea(id: number) {
    this.eliminando = id;
    
    this.http.delete(`/api/tareas/${id}`)
      .pipe(
        catchError((error: HttpErrorResponse) => {
          this.mostrarNotificacion(
            'No se pudo eliminar la tarea. Intenta de nuevo.',
            'error'
          );
          return throwError(() => error);
        })
      )
      .subscribe({
        next: () => {
          this.tareas = this.tareas.filter(t => t.id !== id);
          this.mostrarNotificacion('Tarea eliminada correctamente', 'success');
          this.eliminando = null;
        },
        error: () => {
          this.eliminando = null;
        }
      });
  }

  private mostrarNotificacion(mensaje: string, tipo: 'success' | 'error') {
    this.notificacion = { mensaje, tipo };
    
    // Ocultar notificación después de 4 segundos
    setTimeout(() => {
      this.notificacion = null;
    }, 4000);
  }

  private cargarTareas() {
    this.http.get<any[]>('/api/tareas')
      .pipe(
        catchError(() => {
          this.mostrarNotificacion(
            'Error al cargar las tareas',
            'error'
          );
          return of([]);
        })
      )
      .subscribe(tareas => {
        this.tareas = tareas;
      });
  }
}

Estados de error granulares

Para formularios complejos o interfaces con múltiples secciones, es útil manejar errores de forma granular:

@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="dashboard">
      <section class="seccion-ventas">
        <h2>Ventas del mes</h2>
        @if (errores.ventas) {
          <div class="error-inline">
            {{ errores.ventas }}
            <button (click)="cargarVentas()" class="btn-pequeño">Reintentar</button>
          </div>
        } @else if (cargando.ventas) {
          <div class="loading-pequeño">Cargando...</div>
        } @else {
          <div class="datos-ventas">{{ datosVentas.total }}</div>
        }
      </section>

      <section class="seccion-productos">
        <h2>Productos populares</h2>
        @if (errores.productos) {
          <div class="error-inline">
            {{ errores.productos }}
            <button (click)="cargarProductos()" class="btn-pequeño">Reintentar</button>
          </div>
        } @else if (cargando.productos) {
          <div class="loading-pequeño">Cargando...</div>
        } @else {
          @for (producto of productosPopulares; track producto.id) {
            <div>{{ producto.nombre }}</div>
          }
        }
      </section>
    </div>
  `
})
export class DashboardComponent {
  datosVentas: any = {};
  productosPopulares: any[] = [];
  
  cargando = {
    ventas: false,
    productos: false
  };
  
  errores = {
    ventas: null as string | null,
    productos: null as string | null
  };

  constructor(private http: HttpClient) {
    this.cargarDatos();
  }

  cargarDatos() {
    this.cargarVentas();
    this.cargarProductos();
  }

  cargarVentas() {
    this.cargando.ventas = true;
    this.errores.ventas = null;

    this.http.get('/api/dashboard/ventas')
      .pipe(
        catchError(() => {
          this.errores.ventas = 'Error al cargar datos de ventas';
          return of({});
        })
      )
      .subscribe(data => {
        this.datosVentas = data;
        this.cargando.ventas = false;
      });
  }

  cargarProductos() {
    this.cargando.productos = true;
    this.errores.productos = null;

    this.http.get<any[]>('/api/dashboard/productos')
      .pipe(
        catchError(() => {
          this.errores.productos = 'Error al cargar productos populares';
          return of([]);
        })
      )
      .subscribe(data => {
        this.productosPopulares = data;
        this.cargando.productos = false;
      });
  }
}

Estilos CSS para feedback visual

Complementar los mensajes con estilos apropiados mejora significativamente la experiencia del usuario:

.error {
  background-color: #fee;
  border: 1px solid #f88;
  color: #c44;
  padding: 1rem;
  border-radius: 4px;
  margin: 1rem 0;
}

.error-inline {
  background-color: #fff5f5;
  border-left: 4px solid #f56565;
  padding: 0.5rem 1rem;
  margin: 0.5rem 0;
  font-size: 0.9rem;
}

.alert-success {
  background-color: #f0fff4;
  border: 1px solid #68d391;
  color: #2d7748;
  padding: 1rem;
  border-radius: 4px;
}

.notificacion {
  position: fixed;
  top: 20px;
  right: 20px;
  padding: 1rem 1.5rem;
  border-radius: 4px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  z-index: 1000;
}

.notificacion.success {
  background-color: #f0fff4;
  color: #2d7748;
  border: 1px solid #68d391;
}

.notificacion.error {
  background-color: #fff5f5;
  color: #c53030;
  border: 1px solid #f56565;
}

.btn-reintentar {
  background-color: #4299e1;
  color: white;
  border: none;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  cursor: pointer;
  margin-top: 0.5rem;
}

La clave está en transformar errores técnicos en mensajes comprensibles, proporcionar acciones claras para resolverlos, y mantener feedback visual consistente que guide al usuario hacia la resolución del problema.

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en Angular

Documentación oficial de Angular
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Angular

Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Comprender el uso básico del operador catchError para interceptar errores en peticiones HTTP.
  • Aprender a devolver valores por defecto para evitar fallos en la aplicación.
  • Diferenciar y manejar distintos tipos de errores HTTP usando HttpErrorResponse.
  • Implementar la propagación de errores cuando sea necesario para un manejo avanzado.
  • Diseñar interfaces que muestren mensajes de error claros y feedback visual adecuado al usuario.