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
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.