diff --git a/modules/effects/spec/effects_error_handler.spec.ts b/modules/effects/spec/effects_error_handler.spec.ts index 2d768a7cb1..984fa2d979 100644 --- a/modules/effects/spec/effects_error_handler.spec.ts +++ b/modules/effects/spec/effects_error_handler.spec.ts @@ -1,20 +1,21 @@ -import { ErrorHandler, Provider } from '@angular/core'; +import { ErrorHandler, Provider, Type } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { Action, Store } from '@ngrx/store'; -import { Observable, of } from 'rxjs'; +import { Observable, of, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { createEffect, EFFECTS_ERROR_HANDLER, EffectsModule } from '..'; +import * as effectsSrc from '../src/effects_error_handler'; describe('Effects Error Handler', () => { let subscriptionCount: number; let globalErrorHandler: jasmine.Spy; let storeNext: jasmine.Spy; - function makeEffectTestBed(...providers: Provider[]) { + function makeEffectTestBed(effect: Type, ...providers: Provider[]) { subscriptionCount = 0; TestBed.configureTestingModule({ - imports: [EffectsModule.forRoot([ErrorEffect])], + imports: [EffectsModule.forRoot([effect])], providers: [ { provide: Store, @@ -38,8 +39,14 @@ describe('Effects Error Handler', () => { storeNext = store.next; } + it('should retry on infinite error up to 10 times', () => { + makeEffectTestBed(AlwaysErrorEffect); + + expect(globalErrorHandler.calls.count()).toBe(10); + }); + it('should retry and notify error handler when effect error handler is not provided', () => { - makeEffectTestBed(); + makeEffectTestBed(ErrorEffect); // two subscriptions expected: // 1. Initial subscription to the effect (this will error) @@ -62,7 +69,7 @@ describe('Effects Error Handler', () => { ); }); - makeEffectTestBed({ + makeEffectTestBed(ErrorEffect, { provide: EFFECTS_ERROR_HANDLER, useValue: effectsErrorHandlerSpy, }); @@ -84,6 +91,10 @@ describe('Effects Error Handler', () => { }); } + class AlwaysErrorEffect { + effect$ = createEffect(() => throwError('always an error')); + } + /** * This observable factory returns an observable that will never emit, but the first subscriber will get an immediate * error. All subsequent subscribers will just get an observable that does not emit. diff --git a/modules/effects/src/effects_error_handler.ts b/modules/effects/src/effects_error_handler.ts index 32d19bba3e..910977eb5c 100644 --- a/modules/effects/src/effects_error_handler.ts +++ b/modules/effects/src/effects_error_handler.ts @@ -8,17 +8,25 @@ export type EffectsErrorHandler = ( errorHandler: ErrorHandler ) => Observable; -export const defaultEffectsErrorHandler: EffectsErrorHandler = < - T extends Action ->( +const MAX_NUMBER_OF_RETRY_ATTEMPTS = 10; + +export function defaultEffectsErrorHandler( observable$: Observable, - errorHandler: ErrorHandler -): Observable => { + errorHandler: ErrorHandler, + retryAttemptLeft: number = MAX_NUMBER_OF_RETRY_ATTEMPTS +): Observable { return observable$.pipe( catchError(error => { if (errorHandler) errorHandler.handleError(error); + if (retryAttemptLeft <= 1) { + return observable$; // last attempt + } // Return observable that produces this particular effect - return defaultEffectsErrorHandler(observable$, errorHandler); + return defaultEffectsErrorHandler( + observable$, + errorHandler, + retryAttemptLeft - 1 + ); }) ); -}; +} diff --git a/modules/effects/src/index.ts b/modules/effects/src/index.ts index c84586adca..8c5b0a3c6c 100644 --- a/modules/effects/src/index.ts +++ b/modules/effects/src/index.ts @@ -3,7 +3,10 @@ export { EffectConfig } from './models'; export { Effect } from './effect_decorator'; export { getEffectsMetadata } from './effects_metadata'; export { mergeEffects } from './effects_resolver'; -export { EffectsErrorHandler } from './effects_error_handler'; +export { + EffectsErrorHandler, + defaultEffectsErrorHandler, +} from './effects_error_handler'; export { EffectsMetadata, CreateEffectMetadata } from './models'; export { Actions, ofType } from './actions'; export { EffectsModule } from './effects_module'; diff --git a/projects/ngrx.io/content/guide/effects/lifecycle.md b/projects/ngrx.io/content/guide/effects/lifecycle.md index 09e6050359..60fe61d929 100644 --- a/projects/ngrx.io/content/guide/effects/lifecycle.md +++ b/projects/ngrx.io/content/guide/effects/lifecycle.md @@ -43,7 +43,8 @@ export class LogEffects { Starting with version 8, when an error happens in the effect's main stream it is reported using Angular's `ErrorHandler`, and the source effect is **automatically** resubscribed to (instead of completing), so it continues to -listen to all dispatched Actions. +listen to all dispatched Actions. By default, effects are resubscribed up to 10 +errors. Generally, errors should be handled by users. However, for the cases where errors were missed, this new behavior adds an additional safety net. @@ -96,7 +97,8 @@ The behavior of the default resubscription handler can be customized by providing a custom handler using the `EFFECTS_ERROR_HANDLER` injection token. This allows you to provide a custom behavior, such as only retrying on -certain "retryable" errors, or with maximum number of retries. +certain "retryable" errors, or change the maximum number of retries (it's set to +10 by default). import { ErrorHandler, NgModule } from '@angular/core';