Deprecated : This operator will be removed in v9 or v10. Use retry with the delay option instead. For retryWhen(() => notify$), use retry({ delay: () => notify$ }).
Overview
retryWhen allows you to retry a failed Observable based on custom criteria by providing a notifier function. When the source errors, the error is emitted to a notifier Observable, which can control whether and when to retry.
While this operator is deprecated, understanding it is useful for maintaining existing code. New code should use retry({ delay: () => notifier$ }) instead.
Type Signature
export function retryWhen < T >(
notifier : ( errors : Observable < any >) => ObservableInput < any >
) : MonoTypeOperatorFunction < T >
Parameters
notifier
(errors: Observable<any>) => ObservableInput<any>
required
A function that receives an Observable of errors and returns an Observable.
When the returned Observable emits, the source is retried
When the returned Observable completes, the output completes without error
When the returned Observable errors, that error is propagated
Returns
MonoTypeOperatorFunction<T> - An operator function that returns an Observable that mirrors the source, retrying based on the notifier Observable’s emissions.
Usage Examples
Basic Example: Retry with Delay
Basic RetryWhen
Modern Alternative (Preferred)
import { interval , map , retryWhen , tap , delayWhen , timer } from 'rxjs' ;
const source = interval ( 1000 );
const result = source . pipe (
map ( value => {
if ( value > 5 ) {
throw value ;
}
return value ;
}),
retryWhen ( errors =>
errors . pipe (
tap ( value => console . log ( `Value ${ value } was too high!` )),
delayWhen ( value => timer ( value * 1000 ))
)
)
);
result . subscribe ( value => console . log ( value ));
// Output:
// 0
// 1
// 2
// 3
// 4
// 5
// 'Value 6 was too high!'
// ... wait 6 seconds then repeat ...
Real-World Example: Conditional Retry Logic
import { ajax } from 'rxjs/ajax' ;
import { retryWhen , tap , delayWhen , timer , throwError , take } from 'rxjs' ;
interface ApiError {
status : number ;
message : string ;
}
function fetchDataWithConditionalRetry ( url : string ) : Observable < any > {
return ajax . getJSON ( url ). pipe (
retryWhen ( errors =>
errors . pipe (
tap ( err => console . log ( 'Error occurred:' , err )),
// Analyze error and decide retry strategy
delayWhen (( err : ApiError ) => {
// Retry immediately on network errors
if ( err . status === 0 ) {
console . log ( 'Network error, retrying immediately...' );
return timer ( 0 );
}
// Wait longer for server errors
if ( err . status >= 500 ) {
console . log ( 'Server error, waiting 5s before retry...' );
return timer ( 5000 );
}
// Don't retry client errors
if ( err . status >= 400 && err . status < 500 ) {
console . log ( 'Client error, not retrying' );
return throwError (() => err );
}
// Default: wait 2s
return timer ( 2000 );
}),
take ( 3 ) // Maximum 3 retries
)
)
);
}
fetchDataWithConditionalRetry ( '/api/data' ). subscribe ({
next : data => console . log ( 'Data loaded:' , data ),
error : err => console . error ( 'Failed after retries:' , err )
});
Exponential Backoff with Max Attempts
import { ajax } from 'rxjs/ajax' ;
import { retryWhen , scan , delayWhen , timer , throwError , tap } from 'rxjs' ;
function fetchWithExponentialBackoff ( url : string ) : Observable < any > {
return ajax . getJSON ( url ). pipe (
retryWhen ( errors =>
errors . pipe (
scan (( retryCount , err ) => {
if ( retryCount >= 5 ) {
throw err ; // Max retries reached
}
return retryCount + 1 ;
}, 0 ),
tap ( retryCount => {
const delay = Math . min ( 1000 * Math . pow ( 2 , retryCount - 1 ), 30000 );
console . log ( `Retry attempt ${ retryCount } , waiting ${ delay } ms` );
}),
delayWhen ( retryCount => {
const delay = Math . min ( 1000 * Math . pow ( 2 , retryCount - 1 ), 30000 );
return timer ( delay );
})
)
)
);
}
fetchWithExponentialBackoff ( '/api/users' ). subscribe ({
next : data => console . log ( 'Success:' , data ),
error : err => console . error ( 'Failed after all retries:' , err )
});
// Retry delays:
// Attempt 1: 1000ms
// Attempt 2: 2000ms
// Attempt 3: 4000ms
// Attempt 4: 8000ms
// Attempt 5: 16000ms
User-Controlled Retry
import { ajax } from 'rxjs/ajax' ;
import { retryWhen , switchMap , tap } from 'rxjs' ;
import { Subject } from 'rxjs' ;
const retryClick$ = new Subject < void >();
function fetchDataWithManualRetry ( url : string ) : Observable < any > {
return ajax . getJSON ( url ). pipe (
retryWhen ( errors =>
errors . pipe (
tap ( err => {
console . error ( 'Request failed:' , err . message );
showRetryButton (); // Show UI button
}),
switchMap (() => retryClick$ ), // Wait for user to click retry
tap (() => {
console . log ( 'Retrying...' );
hideRetryButton ();
})
)
)
);
}
fetchDataWithManualRetry ( '/api/data' ). subscribe ({
next : data => {
console . log ( 'Data loaded:' , data );
displayData ( data );
},
error : err => showErrorMessage ( err )
});
// Wire up retry button
const retryButton = document . getElementById ( 'retry' ) as HTMLButtonElement ;
retryButton . addEventListener ( 'click' , () => retryClick$ . next ());
Practical Scenarios
The notifier Observable must emit (not error) to trigger a retry. If it errors or completes without emitting, no retry occurs.
Scenario 1: Retry Based on Error Type
import { ajax } from 'rxjs/ajax' ;
import { retryWhen , mergeMap , timer , throwError , tap } from 'rxjs' ;
interface RetryableError {
status : number ;
retryable : boolean ;
retryAfter ?: number ;
}
function smartRetry ( url : string ) : Observable < any > {
return ajax . getJSON ( url ). pipe (
retryWhen ( errors =>
errors . pipe (
mergeMap (( err : RetryableError ) => {
// Check Retry-After header for rate limiting
if ( err . status === 429 && err . retryAfter ) {
console . log ( `Rate limited. Retrying after ${ err . retryAfter } ms` );
return timer ( err . retryAfter );
}
// Retry server errors after delay
if ( err . status >= 500 ) {
console . log ( 'Server error, retrying in 3s...' );
return timer ( 3000 );
}
// Don't retry client errors
console . error ( 'Non-retryable error:' , err . status );
return throwError (() => err );
})
)
)
);
}
smartRetry ( '/api/data' ). subscribe ({
next : data => console . log ( 'Success:' , data ),
error : err => console . error ( 'Final error:' , err )
});
Scenario 2: Circuit Breaker Pattern
import { ajax } from 'rxjs/ajax' ;
import { retryWhen , scan , tap , delayWhen , timer , throwError } from 'rxjs' ;
interface CircuitState {
failureCount : number ;
lastFailure : number ;
isOpen : boolean ;
}
function fetchWithCircuitBreaker ( url : string ) : Observable < any > {
const circuitState : CircuitState = {
failureCount: 0 ,
lastFailure: 0 ,
isOpen: false
};
return ajax . getJSON ( url ). pipe (
retryWhen ( errors =>
errors . pipe (
tap ( err => {
circuitState . failureCount ++ ;
circuitState . lastFailure = Date . now ();
// Open circuit after 5 failures
if ( circuitState . failureCount >= 5 ) {
circuitState . isOpen = true ;
console . log ( 'Circuit breaker opened!' );
}
}),
delayWhen (() => {
// If circuit is open, wait 30s before trying again
if ( circuitState . isOpen ) {
const timeSinceLastFailure = Date . now () - circuitState . lastFailure ;
if ( timeSinceLastFailure < 30000 ) {
console . log ( 'Circuit open, waiting...' );
return timer ( 30000 - timeSinceLastFailure );
} else {
// Try to close circuit (half-open state)
console . log ( 'Attempting to close circuit...' );
circuitState . isOpen = false ;
circuitState . failureCount = 0 ;
return timer ( 0 );
}
}
// Normal retry delay
return timer ( 2000 );
})
)
),
tap (() => {
// Success - reset circuit
if ( circuitState . failureCount > 0 ) {
console . log ( 'Circuit breaker reset' );
circuitState . failureCount = 0 ;
circuitState . isOpen = false ;
}
})
);
}
fetchWithCircuitBreaker ( '/api/data' ). subscribe ({
next : data => console . log ( 'Data:' , data ),
error : err => console . error ( 'Error:' , err )
});
Scenario 3: Retry with User Notification
import { ajax } from 'rxjs/ajax' ;
import { retryWhen , tap , scan , delayWhen , timer } from 'rxjs' ;
function fetchWithProgressiveNotifications ( url : string ) : Observable < any > {
return ajax . getJSON ( url ). pipe (
retryWhen ( errors =>
errors . pipe (
scan (( retryCount , err ) => {
const count = retryCount + 1 ;
// Show different notifications based on retry count
if ( count === 1 ) {
showToast ( 'Connection issue, retrying...' );
} else if ( count === 3 ) {
showNotification ( 'Still having trouble connecting...' );
} else if ( count === 5 ) {
showWarning ( 'Multiple connection failures detected' );
}
if ( count >= 10 ) {
throw new Error ( 'Max retries exceeded' );
}
return count ;
}, 0 ),
delayWhen ( retryCount => {
const delay = Math . min ( 1000 * retryCount , 10000 );
return timer ( delay );
})
)
),
tap (() => {
// Success - clear any error notifications
clearNotifications ();
showToast ( 'Connected successfully' );
})
);
}
fetchWithProgressiveNotifications ( '/api/data' ). subscribe ({
next : data => displayData ( data ),
error : err => showError ( 'Unable to connect after multiple attempts' )
});
Behavior Details
Notifier Emissions
Each error triggers an emission to the notifier Observable
When notifier emits, the source is resubscribed
When notifier completes (without error), output completes successfully
When notifier errors, that error is propagated
The notifier function receives a hot Observable of errors. Be careful with operators that might not replay values for late subscribers.
import { throwError , retryWhen , tap , delay } from 'rxjs' ;
let errorCount = 0 ;
throwError (() => new Error ( 'Test' )). pipe (
tap (() => console . log ( `Attempt ${ ++ errorCount } ` )),
retryWhen ( errors =>
errors . pipe (
tap ( err => console . log ( 'Error caught:' , err . message )),
delay ( 1000 ),
take ( 2 ) // Only allow 2 retries
)
)
). subscribe ({
next : x => console . log ( 'Value:' , x ),
complete : () => console . log ( 'Complete' ),
error : err => console . log ( 'Final error:' , err . message )
});
// Output:
// Attempt 1
// Error caught: Test
// Attempt 2
// Error caught: Test
// Attempt 3
// Complete (notifier completes after 2 emissions)
Migration to retry()
Here’s how to migrate from retryWhen to the modern retry operator:
// Old way (retryWhen)
import { retryWhen , delayWhen , timer , tap } from 'rxjs' ;
source$ . pipe (
retryWhen ( errors =>
errors . pipe (
tap ( err => console . log ( 'Error:' , err )),
delayWhen (() => timer ( 1000 ))
)
)
)
// New way (retry with delay)
import { retry , tap } from 'rxjs' ;
source$ . pipe (
retry ({
delay : ( error , retryCount ) => {
console . log ( 'Error:' , error );
return timer ( 1000 );
}
})
)
retry - Modern retry operator with configuration object
catchError - Handle errors with custom recovery logic
repeat - Resubscribe on successful completion
throwError - Create an Observable that errors immediately