9. Excepciones: manejar errores sin romper la experiencia¶
Distinguir fallo de red, respuesta HTTP y mensaje para el usuario.
¿Qué puede fallar?¶
- La red puede estar caída.
- La API puede responder 400, 404 o 500.
- El formato recibido puede no coincidir con el modelo.
- Una regla de negocio puede rechazar la operación.
HttpErrorResponse¶
Cuando una petición falla, Angular entrega un HttpErrorResponse que reúne:
| Propiedad | Contenido |
|---|---|
status |
Código de estado HTTP (400, 404, 500…) |
message |
Descripción técnica generada por Angular |
headers |
Cabeceras de la respuesta |
error |
Cuerpo del error que envió el backend |
status: 0
Suele indicar un problema de red o de CORS: la petición ni siquiera obtuvo una respuesta HTTP.
Manejo en el componente¶
Estado explícito de la interfaz¶
En lugar de varias banderas sueltas (loading, hasError…), un solo estado describe en qué momento está la pantalla:
Cargar con manejo de error¶
import { Component, inject, signal } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Student } from '../../models/student.model';
import { StudentService } from '../../services/student.service';
import { StudentCardComponent } from '../student-card/student-card.component';
@Component({
selector: 'app-student-list',
standalone: true,
imports: [StudentCardComponent],
templateUrl: './student-list.component.html'
})
export class StudentListComponent {
private students = inject(StudentService);
status = signal<'idle' | 'loading' | 'success' | 'error'>('idle');
items = signal<Student[]>([]);
message = signal('');
ngOnInit(): void {
this.load();
}
load(): void {
this.status.set('loading');
this.students.list().subscribe({
next: data => {
this.items.set(data);
this.status.set('success');
},
error: (err: HttpErrorResponse) => {
this.message.set(
err.error?.message ?? 'No fue posible cargar'
);
this.status.set('error');
}
});
}
}
@switch (status()) {
@case ('loading') {
<p>Cargando estudiantes…</p>
}
@case ('error') {
<p class="error">{{ message() }}</p>
<button (click)="load()">Reintentar</button>
}
@case ('success') {
@for (s of items(); track s.id) {
<app-student-card [student]="s" />
} @empty {
<p>No hay estudiantes registrados.</p>
}
}
}
Un signal es un valor que avisa a Angular cuando cambia: se lee llamándolo (status()) y se modifica con set(...). El template se actualiza solo.
Flujo de recuperación¶
flowchart LR
A["1 · La UI inicia la operación<br/>y muestra 'cargando'"] --> B["2 · El servicio realiza<br/>la petición HTTP"]
B --> C{"3 · ¿Resultado?"}
C -- éxito --> D[Presentar datos]
C -- error --> E[Mensaje comprensible]
E --> F["4 · Registrar detalles técnicos<br/>sin exponerlos al usuario"]
Qué no mostrar
No mostrar stack traces, rutas internas ni mensajes SQL. La UI explica qué puede hacer el usuario; los logs conservan el detalle técnico.
Si el backend centraliza sus errores con @ControllerAdvice (ver Manejo de excepciones) y devuelve un cuerpo como { "message": "El correo ya está registrado" }, el frontend puede mostrarlo con err.error?.message.
Idea clave
Capturar el error no significa ocultarlo; significa transformarlo en una respuesta controlada.
Excepciones transversales: política global¶
El interceptor centraliza lo común; el componente conserva el contexto.
Algunos errores se manejan igual en toda la aplicación: si el token expiró (401), siempre hay que ir al login. Repetir esa lógica en cada componente es duplicación. Para eso existe el interceptor: una función que se ejecuta en todas las peticiones HTTP.
import { inject } from '@angular/core';
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { Router } from '@angular/router';
import { catchError, from, switchMap, throwError } from 'rxjs';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
// 401 → token inválido o expirado → redirigir al login
if (error.status === 401) {
return from(router.navigateByUrl('/login')).pipe(
switchMap(() => throwError(() => error))
);
}
// Otros errores → propagar al componente
return throwError(() => error);
})
);
};
provideHttpClient(
withInterceptors([authInterceptor, errorInterceptor])
)
-
Política transversal, no duplicada
El interceptor centraliza la reacción a errores HTTP globales. Cada servicio y componente no necesita repetir lógica de redirección o logging; solo maneja el contexto específico de su operación.
-
El componente conserva su contexto
El interceptor lanza el error (
throwError) hacia arriba. El componente puede capturar mensajes específicos concatchError(o elerrordelsubscribe) para mostrar detalles relevantes al usuario.
Códigos y responsabilidad¶
| Código | Situación | Quién reacciona |
|---|---|---|
401 |
No autenticado / token inválido | Interceptor: redirige a /login |
403 |
Autenticado sin permiso | Propagar; el componente muestra mensaje de acceso denegado |
5xx |
Error de servidor | Propagar; el componente muestra mensaje genérico de fallo |
0 |
Fallo de red / timeout | El componente informa problema de conectividad |
sequenceDiagram
participant C as Componente
participant S as Servicio
participant I as errorInterceptor
participant A as API
C->>S: list()
S->>I: GET /api/students
I->>A: petición
A-->>I: 401 Unauthorized
I->>I: navigateByUrl('/login')
I-->>C: throwError(error)
Note over C: el componente decide<br/>si muestra algo más
Precaución
Excluir el endpoint /login del interceptor de errores para evitar bucles de redirección. La autorización definitiva siempre ocurre en el servidor.
Para ver estas piezas aplicadas a un sistema real, revisa el Caso de estudio.