Retry automático con exponential backoff
Los errores de red son inevitables en aplicaciones web modernas. La conectividad puede fallar momentáneamente, los servidores pueden estar sobrecargados temporalmente, o pueden ocurrir errores transitorios que se resuelven por sí mismos. El patrón de retry automático con exponential backoff nos permite manejar estos casos de forma elegante, reintentando las peticiones fallidas con intervalos progresivamente mayores.
Fundamentos del retry pattern
El retry automático consiste en volver a ejecutar una petición HTTP cuando falla, pero no de forma inmediata. El exponential backoff añade una estrategia inteligente: cada reintento espera el doble de tiempo que el anterior, evitando sobrecargar un servidor que ya puede estar en problemas.
RxJS proporciona varios operadores para implementar retry logic. El operador retry()
es el más básico, pero retryWhen()
nos da control total sobre cuándo y cómo reintentar:
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { retry, retryWhen, delay, take, concat, throwError } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class DataService {
private http = inject(HttpClient);
// Retry básico: 3 intentos inmediatos
getData() {
return this.http.get('/api/data')
.pipe(retry(3));
}
// Retry con delay fijo
getDataWithDelay() {
return this.http.get('/api/data')
.pipe(
retryWhen(errors =>
errors.pipe(
delay(1000),
take(3)
)
)
);
}
}
Implementando exponential backoff
El exponential backoff incrementa progresivamente el tiempo entre reintentos. Una implementación típica duplica el delay en cada intento:
import { timer, switchMap, finalize } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ResilientDataService {
private http = inject(HttpClient);
getDataWithBackoff() {
return this.http.get('/api/data').pipe(
retryWhen(errors =>
errors.pipe(
// Enumerar los intentos para calcular el delay
scan((retryCount, error) => {
// Solo reintentar errores de servidor (5xx) o red
if (retryCount >= 3 || !this.shouldRetry(error)) {
throw error;
}
return retryCount + 1;
}, 0),
// Calcular delay exponencial: 1s, 2s, 4s
switchMap(retryCount =>
timer(Math.pow(2, retryCount) * 1000)
)
)
)
);
}
private shouldRetry(error: any): boolean {
// Reintentar solo errores 5xx o errores de red
return error.status >= 500 || error.status === 0;
}
}
Interceptor para retry global
Para centralizar la lógica de retry en toda la aplicación, podemos crear un interceptor funcional que aplique exponential backoff automáticamente:
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { retryWhen, scan, switchMap, timer, throwError } from 'rxjs';
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
const maxRetries = 3;
const baseDelay = 1000;
return next(req).pipe(
retryWhen(errors =>
errors.pipe(
scan((retryCount, error) => {
console.log(`Intento ${retryCount + 1} fallido:`, error.message);
if (retryCount >= maxRetries || !isRetriableError(error)) {
throw error;
}
return retryCount + 1;
}, 0),
switchMap(retryCount => {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`Reintentando en ${delay}ms...`);
return timer(delay);
})
)
)
);
};
function isRetriableError(error: any): boolean {
// Reintentar errores de servidor o problemas de conectividad
return error.status >= 500 ||
error.status === 0 ||
error.name === 'TimeoutError';
}
Configuración del interceptor
Para aplicar el retry interceptor globalmente, lo registramos en la configuración de provideHttpClient:
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { retryInterceptor } from './app/interceptors/retry.interceptor';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withInterceptors([retryInterceptor])
)
]
});
Retry selectivo por endpoint
En algunos casos necesitamos diferentes estrategias según el endpoint. Podemos hacer el retry más inteligente analizando la URL o agregando headers específicos:
export const smartRetryInterceptor: HttpInterceptorFn = (req, next) => {
// Configuración específica por endpoint
const retryConfig = getRetryConfig(req.url);
return next(req).pipe(
retryWhen(errors =>
errors.pipe(
scan((retryCount, error) => {
if (retryCount >= retryConfig.maxRetries ||
!isRetriableError(error)) {
throw error;
}
return retryCount + 1;
}, 0),
switchMap(retryCount => {
const delay = retryConfig.baseDelay *
Math.pow(retryConfig.backoffFactor, retryCount);
return timer(delay);
})
)
)
);
};
function getRetryConfig(url: string) {
if (url.includes('/api/critical')) {
return { maxRetries: 5, baseDelay: 500, backoffFactor: 1.5 };
}
if (url.includes('/api/analytics')) {
return { maxRetries: 1, baseDelay: 2000, backoffFactor: 2 };
}
return { maxRetries: 3, baseDelay: 1000, backoffFactor: 2 };
}
Integración con signals
Para aplicaciones que usan signals, podemos crear un servicio que exponga el estado de los reintentos de forma reactiva:
@Injectable({
providedIn: 'root'
})
export class RetryStatusService {
private retryCount = signal(0);
private isRetrying = signal(false);
private lastError = signal<string | null>(null);
readonly retryInfo = computed(() => ({
count: this.retryCount(),
isRetrying: this.isRetrying(),
lastError: this.lastError()
}));
updateRetryStatus(count: number, error?: any) {
this.retryCount.set(count);
this.isRetrying.set(count > 0);
this.lastError.set(error?.message || null);
}
resetRetryStatus() {
this.retryCount.set(0);
this.isRetrying.set(false);
this.lastError.set(null);
}
}
El patrón de retry con exponential backoff es esencial para construir aplicaciones resilientes. Permite que nuestras aplicaciones se recuperen automáticamente de errores transitorios sin abrumar los servidores, mejorando significativamente la experiencia del usuario en entornos de red inestables.
Timeout y manejo de errores HTTP
Los timeouts protegen nuestras aplicaciones de peticiones que se cuelgan indefinidamente, mientras que el manejo adecuado de errores HTTP permite ofrecer una experiencia de usuario consistente y profesional. Angular proporciona múltiples niveles de configuración para timeout y herramientas para manejar diferentes tipos de errores de forma centralizada.
Configuración de timeout global
Para establecer un timeout global que aplique a todas las peticiones HTTP, podemos configurarlo directamente en provideHttpClient()
:
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withRequestsMadeViaParent } from '@angular/common/http';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(),
// Configuración global de timeout (no directamente disponible)
// Se debe hacer mediante interceptor
]
});
Como Angular no tiene configuración global nativa de timeout, implementamos un interceptor de timeout que aplique a todas las peticiones:
import { HttpInterceptorFn } from '@angular/common/http';
import { timeout, catchError, throwError } from 'rxjs';
export const timeoutInterceptor: HttpInterceptorFn = (req, next) => {
const timeoutValue = req.headers.get('X-Timeout')
? parseInt(req.headers.get('X-Timeout')!)
: 30000; // 30 segundos por defecto
return next(req).pipe(
timeout(timeoutValue),
catchError(error => {
if (error.name === 'TimeoutError') {
console.error(`Request timeout después de ${timeoutValue}ms:`, req.url);
return throwError(() => ({
...error,
message: 'La petición ha tardado demasiado en responder',
isTimeout: true
}));
}
return throwError(() => error);
})
);
};
Timeout por petición específica
Para casos específicos donde necesitamos timeouts diferentes, podemos configurarlos a nivel de servicio o petición individual:
@Injectable({
providedIn: 'root'
})
export class ApiService {
private http = inject(HttpClient);
// Timeout específico usando RxJS
getDataWithTimeout() {
return this.http.get('/api/data').pipe(
timeout(10000), // 10 segundos
catchError(error => {
if (error.name === 'TimeoutError') {
return throwError(() => new Error('Datos no disponibles temporalmente'));
}
return throwError(() => error);
})
);
}
// Timeout usando headers personalizados
getCriticalData() {
const headers = { 'X-Timeout': '5000' };
return this.http.get('/api/critical', { headers });
}
// Timeout con fallback automático
getDataWithFallback() {
return this.http.get('/api/primary').pipe(
timeout(3000),
catchError(error => {
if (error.name === 'TimeoutError') {
console.warn('Timeout en endpoint principal, usando fallback');
return this.http.get('/api/fallback');
}
return throwError(() => error);
})
);
}
}
Interceptor para manejo centralizado de errores
Un interceptor de manejo de errores nos permite centralizar toda la lógica de error handling y logging:
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
export const errorHandlerInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
// Logging centralizado
logError(error, req.url);
// Manejo por tipo de error
const handledError = handleHttpError(error);
return throwError(() => handledError);
})
);
};
function logError(error: HttpErrorResponse, url: string) {
const errorInfo = {
url,
status: error.status,
message: error.message,
timestamp: new Date().toISOString()
};
if (error.status >= 500) {
console.error('Error de servidor:', errorInfo);
} else if (error.status >= 400) {
console.warn('Error de cliente:', errorInfo);
} else {
console.info('Error de red:', errorInfo);
}
}
function handleHttpError(error: HttpErrorResponse) {
switch (error.status) {
case 0:
return {
...error,
userMessage: 'Sin conexión a internet',
type: 'network'
};
case 401:
return {
...error,
userMessage: 'Sesión expirada',
type: 'auth'
};
case 403:
return {
...error,
userMessage: 'Sin permisos para esta operación',
type: 'permission'
};
case 404:
return {
...error,
userMessage: 'Recurso no encontrado',
type: 'notfound'
};
case 429:
return {
...error,
userMessage: 'Demasiadas peticiones, intenta más tarde',
type: 'ratelimit'
};
default:
return {
...error,
userMessage: error.status >= 500
? 'Error del servidor, intenta más tarde'
: 'Error inesperado',
type: 'unknown'
};
}
}
Servicio de fallback responses
Para mejorar la experiencia de usuario, podemos implementar respuestas de fallback cuando las peticiones principales fallan:
@Injectable({
providedIn: 'root'
})
export class FallbackService {
private http = inject(HttpClient);
private fallbackData = new Map<string, any>();
getDataWithFallback<T>(url: string, fallbackValue?: T) {
return this.http.get<T>(url).pipe(
// Guardar respuesta exitosa como fallback
tap(response => {
this.fallbackData.set(url, response);
}),
catchError(error => {
console.warn(`Error en ${url}, usando fallback:`, error.message);
// Intentar usar datos en caché
const cached = this.fallbackData.get(url);
if (cached) {
return of(cached);
}
// Usar valor de fallback proporcionado
if (fallbackValue !== undefined) {
return of(fallbackValue);
}
// Sin fallback disponible
return throwError(() => error);
})
);
}
// Fallback con datos estáticos
getMenuItems() {
const staticMenu = [
{ id: 1, title: 'Inicio', path: '/' },
{ id: 2, title: 'Productos', path: '/products' }
];
return this.getDataWithFallback('/api/menu', staticMenu);
}
}
Circuit breaker pattern básico
El circuit breaker evita realizar peticiones a servicios que están fallando consistentemente:
@Injectable({
providedIn: 'root'
})
export class CircuitBreakerService {
private failures = new Map<string, number>();
private lastFailure = new Map<string, number>();
private readonly maxFailures = 5;
private readonly timeoutWindow = 60000; // 1 minuto
private http = inject(HttpClient);
get<T>(url: string): Observable<T> {
if (this.isCircuitOpen(url)) {
const timeRemaining = this.getTimeUntilReset(url);
return throwError(() => ({
message: `Servicio temporalmente no disponible. Reintenta en ${Math.ceil(timeRemaining / 1000)}s`,
isCircuitOpen: true,
timeRemaining
}));
}
return this.http.get<T>(url).pipe(
tap(() => {
// Reset en caso de éxito
this.failures.delete(url);
this.lastFailure.delete(url);
}),
catchError(error => {
this.recordFailure(url);
return throwError(() => error);
})
);
}
private isCircuitOpen(url: string): boolean {
const failures = this.failures.get(url) || 0;
const lastFailureTime = this.lastFailure.get(url) || 0;
const timeSinceLastFailure = Date.now() - lastFailureTime;
// Circuit cerrado si ha pasado el tiempo de timeout
if (timeSinceLastFailure > this.timeoutWindow) {
this.failures.delete(url);
this.lastFailure.delete(url);
return false;
}
return failures >= this.maxFailures;
}
private recordFailure(url: string): void {
const current = this.failures.get(url) || 0;
this.failures.set(url, current + 1);
this.lastFailure.set(url, Date.now());
}
private getTimeUntilReset(url: string): number {
const lastFailureTime = this.lastFailure.get(url) || 0;
return Math.max(0, this.timeoutWindow - (Date.now() - lastFailureTime));
}
}
Integración completa de interceptors
Para combinar todos los patterns, organizamos los interceptors en el orden correcto:
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { timeoutInterceptor } from './app/interceptors/timeout.interceptor';
import { retryInterceptor } from './app/interceptors/retry.interceptor';
import { errorHandlerInterceptor } from './app/interceptors/error-handler.interceptor';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withInterceptors([
timeoutInterceptor, // Primero: timeout
retryInterceptor, // Segundo: retry automático
errorHandlerInterceptor // Último: manejo de errores final
])
)
]
});
La combinación de timeout y manejo de errores crea aplicaciones verdaderamente resilientes. Los timeouts evitan que las peticiones se cuelguen indefinidamente, mientras que el manejo inteligente de errores proporciona feedback útil al usuario y permite implementar estrategias de recuperación automática.

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 patrón de retry automático y su implementación con exponential backoff.
- Aprender a crear interceptores para aplicar retry y timeout de forma global.
- Configurar timeouts específicos por petición y manejar errores HTTP centralizadamente.
- Implementar estrategias de fallback y circuit breaker para mejorar la robustez.
- Integrar múltiples interceptores para una gestión completa de errores y reintentos.