Overview
retry automatically resubscribes to the source Observable when it errors, up to a maximum number of retry attempts. It’s useful for handling transient failures like network errors. All emissions from failed attempts are passed through to subscribers.
Perfect for handling temporary network issues or service disruptions. Use with a delay to implement exponential backoff for more resilient retry logic.
Type Signature
export function retry<T>(count?: number): MonoTypeOperatorFunction<T>;
export function retry<T>(config: RetryConfig): MonoTypeOperatorFunction<T>;
Parameters
The maximum number of retry attempts. If omitted or set to Infinity, retries indefinitely until successful.
Configuration object for advanced retry behavior:interface RetryConfig {
count?: number;
delay?: number | ((error: any, retryCount: number) => ObservableInput<any>);
resetOnSuccess?: boolean;
}
count: Maximum retry attempts
delay: Milliseconds to wait between retries, or a function returning a notifier Observable
resetOnSuccess: If true, resets retry counter when a value is successfully emitted
Returns
MonoTypeOperatorFunction<T> - An operator function that returns an Observable that resubscribes to the source on error, up to the specified count.
Usage Examples
Basic Example: Simple Retry
import { interval, mergeMap, throwError, of, retry } from 'rxjs';
const source = interval(1000);
const result = source.pipe(
mergeMap(val => val > 5 ? throwError(() => 'Error!') : of(val)),
retry(2) // retry 2 times on error
);
result.subscribe({
next: value => console.log(value),
error: err => console.log(`${err}: Retried 2 times then quit!`)
});
// Output:
// 0..1..2..3..4..5..
// 0..1..2..3..4..5..
// 0..1..2..3..4..5..
// 'Error!: Retried 2 times then quit!'
Real-World Example: API Request with Exponential Backoff
import { ajax } from 'rxjs/ajax';
import { retry, timer } from 'rxjs';
interface ApiResponse {
data: any;
timestamp: number;
}
function fetchWithExponentialBackoff(url: string): Observable<ApiResponse> {
return ajax.getJSON(url).pipe(
map(data => ({
data,
timestamp: Date.now()
})),
retry({
count: 5,
delay: (error, retryCount) => {
const delayMs = Math.min(1000 * Math.pow(2, retryCount - 1), 30000);
console.log(`Retry attempt ${retryCount} after ${delayMs}ms`);
return timer(delayMs);
}
})
);
}
fetchWithExponentialBackoff('/api/users').subscribe({
next: response => {
console.log('Success:', response);
displayData(response.data);
},
error: err => {
console.error('Failed after all retries:', err);
showErrorMessage('Unable to load data');
}
});
// Retry delays:
// Attempt 1: 1000ms (1s)
// Attempt 2: 2000ms (2s)
// Attempt 3: 4000ms (4s)
// Attempt 4: 8000ms (8s)
// Attempt 5: 16000ms (16s)
WebSocket with Auto-Reconnect
import { webSocket } from 'rxjs/webSocket';
import { retry, tap, delay } from 'rxjs';
interface WebSocketMessage {
type: string;
data: any;
timestamp: number;
}
function connectWithRetry(url: string): Observable<WebSocketMessage> {
return webSocket<WebSocketMessage>(url).pipe(
tap({
next: msg => console.log('Received:', msg),
error: err => console.warn('WebSocket error:', err)
}),
retry({
delay: (error, retryCount) => {
console.log(`Reconnecting... (attempt ${retryCount})`);
return timer(2000); // Wait 2s before each reconnect
}
})
);
}
connectWithRetry('ws://localhost:8080/live').subscribe({
next: message => handleMessage(message),
error: err => console.error('Connection failed permanently:', err)
});
File Upload with Retry Logic
import { ajax } from 'rxjs/ajax';
import { retry, tap, catchError, throwError } from 'rxjs';
interface UploadResult {
success: boolean;
url?: string;
error?: string;
}
function uploadFileWithRetry(file: File): Observable<UploadResult> {
const formData = new FormData();
formData.append('file', file);
let attemptCount = 0;
return ajax({
url: '/api/upload',
method: 'POST',
body: formData
}).pipe(
map(response => ({
success: true,
url: response.response.url
})),
tap({
error: err => {
attemptCount++;
console.log(`Upload attempt ${attemptCount} failed:`, err.message);
}
}),
retry({
count: 3,
delay: (error, retryCount) => {
// Don't retry on client errors (4xx)
if (error.status >= 400 && error.status < 500) {
console.error('Client error, not retrying:', error.status);
return throwError(() => error);
}
// Retry on server errors (5xx) or network errors
const delayMs = retryCount * 1000;
console.log(`Retrying upload in ${delayMs}ms...`);
return timer(delayMs);
}
}),
catchError(err => of({
success: false,
error: err.message
}))
);
}
const fileInput = document.getElementById('file') as HTMLInputElement;
const file = fileInput.files![0];
uploadFileWithRetry(file).subscribe((result: UploadResult) => {
if (result.success) {
console.log('Upload successful:', result.url);
showSuccessMessage('File uploaded successfully');
} else {
console.error('Upload failed:', result.error);
showErrorMessage(`Upload failed: ${result.error}`);
}
});
Practical Scenarios
Use resetOnSuccess when dealing with long-running Observables where you want to retry transient errors but not count successful emissions against the retry limit.
Scenario 1: Database Query with Retry on Deadlock
import { retry, timer } from 'rxjs';
function executeQuery(sql: string): Observable<any> {
return ajax.post('/api/db/query', { sql }).pipe(
retry({
count: 5,
delay: (error, retryCount) => {
// Check if error is a deadlock
if (error.status === 409 || error.response?.code === 'DEADLOCK') {
// Random delay between 100-500ms to avoid repeated deadlocks
const randomDelay = Math.random() * 400 + 100;
console.log(`Deadlock detected, retrying in ${randomDelay.toFixed(0)}ms`);
return timer(randomDelay);
}
// For other errors, don't retry
return throwError(() => error);
}
})
);
}
executeQuery('SELECT * FROM users WHERE id = 123').subscribe({
next: result => console.log('Query result:', result),
error: err => console.error('Query failed:', err)
});
Scenario 2: Streaming Data with Resilience
import { interval, switchMap, retry } from 'rxjs';
import { ajax } from 'rxjs/ajax';
interface SensorData {
temperature: number;
humidity: number;
timestamp: number;
}
function monitorSensor(sensorId: string): Observable<SensorData> {
return interval(5000).pipe(
switchMap(() =>
ajax.getJSON<SensorData>(`/api/sensors/${sensorId}/data`)
),
retry({
delay: (error, retryCount) => {
console.warn(`Sensor read failed (attempt ${retryCount}), retrying...`);
return timer(2000);
},
resetOnSuccess: true // Reset counter on each successful read
})
);
}
monitorSensor('sensor-001').subscribe({
next: data => {
console.log('Sensor data:', data);
updateDashboard(data);
},
error: err => {
console.error('Sensor monitoring failed:', err);
showSensorError('sensor-001');
}
});
Scenario 3: Authentication Token Refresh
import { ajax } from 'rxjs/ajax';
import { retry, switchMap, catchError, throwError } from 'rxjs';
interface AuthTokens {
accessToken: string;
refreshToken: string;
expiresIn: number;
}
let tokens: AuthTokens | null = null;
function refreshAuthToken(): Observable<AuthTokens> {
if (!tokens?.refreshToken) {
return throwError(() => new Error('No refresh token available'));
}
return ajax.post<AuthTokens>('/api/auth/refresh', {
refreshToken: tokens.refreshToken
}).pipe(
map(response => response.response),
tap(newTokens => {
tokens = newTokens;
localStorage.setItem('tokens', JSON.stringify(newTokens));
}),
retry({
count: 2,
delay: 1000
}),
catchError(err => {
// If refresh fails, redirect to login
console.error('Token refresh failed:', err);
redirectToLogin();
return throwError(() => new Error('Authentication failed'));
})
);
}
function authenticatedRequest(url: string): Observable<any> {
return ajax.getJSON(url, {
Authorization: `Bearer ${tokens?.accessToken}`
}).pipe(
catchError(err => {
// If 401, try refreshing token and retry
if (err.status === 401) {
console.log('Access token expired, refreshing...');
return refreshAuthToken().pipe(
switchMap(() => authenticatedRequest(url))
);
}
return throwError(() => err);
})
);
}
authenticatedRequest('/api/user/profile').subscribe({
next: profile => displayProfile(profile),
error: err => showError(err.message)
});
Scenario 4: Batch Processing with Per-Item Retry
import { from, mergeMap, retry, catchError, of } from 'rxjs';
interface ProcessingResult {
id: string;
success: boolean;
data?: any;
error?: string;
}
function processItem(id: string): Observable<ProcessingResult> {
return ajax.post(`/api/process/${id}`, {}).pipe(
map(response => ({
id,
success: true,
data: response.response
})),
retry({
count: 2,
delay: 500
}),
catchError(err => of({
id,
success: false,
error: err.message
}))
);
}
function processBatch(itemIds: string[]): Observable<ProcessingResult[]> {
return from(itemIds).pipe(
mergeMap(
id => processItem(id),
5 // Process 5 items concurrently
),
scan((results, result) => [...results, result], [] as ProcessingResult[])
);
}
processBatch(['item1', 'item2', 'item3', 'item4', 'item5']).subscribe({
next: results => {
const successful = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
console.log(`Processed: ${successful} successful, ${failed} failed`);
},
complete: () => console.log('Batch processing complete')
});
Behavior Details
Retry Behavior
- Resubscribes to the source Observable on error
- All emissions from failed attempts are passed through
- After max retries, the error is propagated
- Successful completion stops retry logic
Without a count limit, retry() will retry infinitely. Always set a reasonable retry limit for production code.
import { throwError, retry, tap } from 'rxjs';
let attempts = 0;
throwError(() => new Error('Always fails')).pipe(
tap(() => console.log(`Attempt ${++attempts}`)),
retry(3)
).subscribe({
error: err => console.log(`Failed after ${attempts} total attempts`)
});
// Output:
// Attempt 1
// Attempt 2
// Attempt 3
// Attempt 4
// Failed after 4 total attempts
Reset On Success
import { interval, map, retry } from 'rxjs';
let errorCount = 0;
interval(1000).pipe(
map(x => {
// Fail every 5th emission
if (x % 5 === 0 && x !== 0) {
errorCount++;
throw new Error(`Error at ${x}`);
}
return x;
}),
retry({
count: 3,
resetOnSuccess: true // Reset counter after each successful emission
})
).subscribe({
next: x => console.log('Value:', x),
error: err => console.log('Failed:', err.message)
});
// With resetOnSuccess: true, can handle multiple error cycles
// Without it, would fail after 3 total errors
retryWhen - Retry with custom observable logic (deprecated)
catchError - Handle errors with custom logic
throwError - Create an error Observable
repeat - Resubscribe on successful completion