Saltar a contenido

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:

status = signal<
  'idle' | 'loading' |
  'success' | 'error'
>('idle');

Cargar con manejo de error

src/app/students/student-list/student-list.component.ts
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');
      }
    });
  }
}
src/app/students/student-list/student-list.component.html
@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.

src/app/core/error.interceptor.ts — HttpInterceptorFn
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);
    })
  );
};
src/app/app.config.ts — registro
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 con catchError (o el error del subscribe) 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.

if (error.status === 401 && !req.url.includes('/login')) { ... }

Para ver estas piezas aplicadas a un sistema real, revisa el Caso de estudio.