diff --git a/docs/effects/api.md b/docs/effects/api.md index 99db895942..ecae318702 100644 --- a/docs/effects/api.md +++ b/docs/effects/api.md @@ -204,6 +204,62 @@ export class UserEffects implements OnRunEffects { } ``` +### EffectsErrorHandler + +By default, if an effect has `{useEffectsErrorHandler: true}` (the effect metadata default), when the effect encounters an error it +is automatically resubscribed to, and the Angular `ErrorHandler.handleError` method is called with the error, and the +effect observable resubscribed to. + +If you want to customize this behavior, for example if you have a [custom error handler](https://angular.io/api/core/ErrorHandler) that needs specific input, or you +only want to resubscribe on certain errors etc, you may provide a custom handler using the `EFFECTS_ERROR_HANDLER` +injection token. + +Usage: + +```ts +import { EffectsModule, EFFECTS_ERROR_HANDLER } from '@ngrx/effects'; +import { MovieEffects } from './effects/movie.effects'; +import { CustomErrorHandler, isRetryable } from '../custom-error-handler'; +import { Action } from '@ngrx/store'; +import { Observable, throwError } from 'rxjs'; +import { retryWhen, mergeMap } from 'rxjs/operators'; + +export function effectResubscriptionHandler( + observable$: Observable, + errorHandler?: CustomErrorHandler +): Observable { + return observable$.pipe( + retryWhen(errors => + errors.pipe( + mergeMap(e => { + if (isRetryable(e)) { + return errorHandler.handleRetryableError(e); + } + + errorHandler.handleError(e); + return throwError(e); + }) + ) + ) + ); +} + +@NgModule({ + imports: [EffectsModule.forRoot([MovieEffects])], + providers: [ + { + provide: EFFECTS_ERROR_HANDLER, + useValue: effectResubscriptionHandler, + }, + { + provide: ErrorHandle, + useClass: CustomErrorHandler, + }, + ], +}) +export class AppModule {} +``` + ## Utilities ### mergeEffects diff --git a/modules/effects/spec/effect_creator.spec.ts b/modules/effects/spec/effect_creator.spec.ts index 70264ed88e..f56236d055 100644 --- a/modules/effects/spec/effect_creator.spec.ts +++ b/modules/effects/spec/effect_creator.spec.ts @@ -52,30 +52,32 @@ describe('createEffect()', () => { a = createEffect(() => of({ type: 'a' })); b = createEffect(() => of({ type: 'b' }), { dispatch: true }); c = createEffect(() => of({ type: 'c' }), { dispatch: false }); - d = createEffect(() => of({ type: 'd' }), { resubscribeOnError: true }); + d = createEffect(() => of({ type: 'd' }), { + useEffectsErrorHandler: true, + }); e = createEffect(() => of({ type: 'd' }), { - resubscribeOnError: false, + useEffectsErrorHandler: false, }); f = createEffect(() => of({ type: 'e' }), { dispatch: false, - resubscribeOnError: false, + useEffectsErrorHandler: false, }); g = createEffect(() => of({ type: 'e' }), { dispatch: true, - resubscribeOnError: false, + useEffectsErrorHandler: false, }); } const mock = new Fixture(); expect(getCreateEffectMetadata(mock)).toEqual([ - { propertyName: 'a', dispatch: true, resubscribeOnError: true }, - { propertyName: 'b', dispatch: true, resubscribeOnError: true }, - { propertyName: 'c', dispatch: false, resubscribeOnError: true }, - { propertyName: 'd', dispatch: true, resubscribeOnError: true }, - { propertyName: 'e', dispatch: true, resubscribeOnError: false }, - { propertyName: 'f', dispatch: false, resubscribeOnError: false }, - { propertyName: 'g', dispatch: true, resubscribeOnError: false }, + { propertyName: 'a', dispatch: true, useEffectsErrorHandler: true }, + { propertyName: 'b', dispatch: true, useEffectsErrorHandler: true }, + { propertyName: 'c', dispatch: false, useEffectsErrorHandler: true }, + { propertyName: 'd', dispatch: true, useEffectsErrorHandler: true }, + { propertyName: 'e', dispatch: true, useEffectsErrorHandler: false }, + { propertyName: 'f', dispatch: false, useEffectsErrorHandler: false }, + { propertyName: 'g', dispatch: true, useEffectsErrorHandler: false }, ]); }); diff --git a/modules/effects/spec/effect_decorator.spec.ts b/modules/effects/spec/effect_decorator.spec.ts index 7b91245a98..80cf15bdde 100644 --- a/modules/effects/spec/effect_decorator.spec.ts +++ b/modules/effects/spec/effect_decorator.spec.ts @@ -9,26 +9,26 @@ describe('@Effect()', () => { b: any; @Effect({ dispatch: false }) c: any; - @Effect({ resubscribeOnError: true }) + @Effect({ useEffectsErrorHandler: true }) d: any; - @Effect({ resubscribeOnError: false }) + @Effect({ useEffectsErrorHandler: false }) e: any; - @Effect({ dispatch: false, resubscribeOnError: false }) + @Effect({ dispatch: false, useEffectsErrorHandler: false }) f: any; - @Effect({ dispatch: true, resubscribeOnError: false }) + @Effect({ dispatch: true, useEffectsErrorHandler: false }) g: any; } const mock = new Fixture(); expect(getEffectDecoratorMetadata(mock)).toEqual([ - { propertyName: 'a', dispatch: true, resubscribeOnError: true }, - { propertyName: 'b', dispatch: true, resubscribeOnError: true }, - { propertyName: 'c', dispatch: false, resubscribeOnError: true }, - { propertyName: 'd', dispatch: true, resubscribeOnError: true }, - { propertyName: 'e', dispatch: true, resubscribeOnError: false }, - { propertyName: 'f', dispatch: false, resubscribeOnError: false }, - { propertyName: 'g', dispatch: true, resubscribeOnError: false }, + { propertyName: 'a', dispatch: true, useEffectsErrorHandler: true }, + { propertyName: 'b', dispatch: true, useEffectsErrorHandler: true }, + { propertyName: 'c', dispatch: false, useEffectsErrorHandler: true }, + { propertyName: 'd', dispatch: true, useEffectsErrorHandler: true }, + { propertyName: 'e', dispatch: true, useEffectsErrorHandler: false }, + { propertyName: 'f', dispatch: false, useEffectsErrorHandler: false }, + { propertyName: 'g', dispatch: true, useEffectsErrorHandler: false }, ]); }); diff --git a/modules/effects/spec/effect_sources.spec.ts b/modules/effects/spec/effect_sources.spec.ts index f50797b1f0..26aec434a5 100644 --- a/modules/effects/spec/effect_sources.spec.ts +++ b/modules/effects/spec/effect_sources.spec.ts @@ -263,9 +263,9 @@ describe('EffectSources', () => { expect(toActions(sources$)).toBeObservable(expected); }); - it('should not resubscribe on error when resubscribeOnError is false', () => { + it('should not resubscribe on error when useEffectsErrorHandler is false', () => { class Eff { - @Effect({ resubscribeOnError: false }) + @Effect({ useEffectsErrorHandler: false }) b$ = hot('a--b--c--d').pipe( map(v => { if (v == 'b') throw new Error('An Error'); @@ -552,7 +552,7 @@ describe('EffectSources', () => { expect(toActions(sources$)).toBeObservable(expected); }); - it('should not resubscribe on error when resubscribeOnError is false', () => { + it('should not resubscribe on error when useEffectsErrorHandler is false', () => { const sources$ = of( new class { b$ = createEffect( @@ -563,7 +563,7 @@ describe('EffectSources', () => { return v; }) ), - { dispatch: false, resubscribeOnError: false } + { dispatch: false, useEffectsErrorHandler: false } ); }() ); diff --git a/modules/effects/spec/effects_error_handler.spec.ts b/modules/effects/spec/effects_error_handler.spec.ts new file mode 100644 index 0000000000..065c7cc8ba --- /dev/null +++ b/modules/effects/spec/effects_error_handler.spec.ts @@ -0,0 +1,99 @@ +import { ErrorHandler, Provider } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { Action, Store } from '@ngrx/store'; +import { Observable, of } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { createEffect, EFFECTS_ERROR_HANDLER, EffectsModule } from '..'; + +describe('NgRx Effects Error Handler spec', () => { + let subscriptionCount: number; + let globalErrorHandler: jasmine.Spy; + let storeNext: jasmine.Spy; + + function makeEffectTestBed(...providers: Provider[]) { + subscriptionCount = 0; + + TestBed.configureTestingModule({ + imports: [EffectsModule.forRoot([ErrorEffect])], + providers: [ + { + provide: Store, + useValue: { + next: jasmine.createSpy('storeNext'), + }, + }, + { + provide: ErrorHandler, + useValue: { + handleError: jasmine.createSpy('globalErrorHandler'), + }, + }, + ...providers, + ], + }); + + globalErrorHandler = TestBed.get(ErrorHandler).handleError; + const store = TestBed.get(Store); + storeNext = store.next; + } + + it('should retry and notify error handler when effect error handler is not provided', () => { + makeEffectTestBed(); + + // two subscriptions expected: + // 1. Initial subscription to the effect (this will error) + // 2. Resubscription to the effect after error (this will not error) + expect(subscriptionCount).toBe(2); + expect(globalErrorHandler).toHaveBeenCalledWith(new Error('effectError')); + }); + + it('should use custom error behavior when EFFECTS_ERROR_HANDLER is provided', () => { + const effectsErrorHandlerSpy = jasmine + .createSpy() + .and.callFake((effect$: Observable, errorHandler: ErrorHandler) => { + return effect$.pipe( + catchError(err => { + errorHandler.handleError( + new Error('inside custom handler: ' + err.message) + ); + return of({ type: 'custom action' }); + }) + ); + }); + + makeEffectTestBed({ + provide: EFFECTS_ERROR_HANDLER, + useValue: effectsErrorHandlerSpy, + }); + + expect(effectsErrorHandlerSpy).toHaveBeenCalledWith( + jasmine.any(Observable), + TestBed.get(ErrorHandler) + ); + expect(globalErrorHandler).toHaveBeenCalledWith( + new Error('inside custom handler: effectError') + ); + expect(subscriptionCount).toBe(1); + expect(storeNext).toHaveBeenCalledWith({ type: 'custom action' }); + }); + + class ErrorEffect { + effect$ = createEffect(errorFirstSubscriber, { + useEffectsErrorHandler: true, + }); + } + + /** + * 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. + */ + function errorFirstSubscriber(): Observable { + return new Observable(observer => { + subscriptionCount++; + + if (subscriptionCount === 1) { + observer.error(new Error('effectError')); + } + }); + } +}); diff --git a/modules/effects/spec/effects_metadata.spec.ts b/modules/effects/spec/effects_metadata.spec.ts index 5cc2e141e0..af5afb7783 100644 --- a/modules/effects/spec/effects_metadata.spec.ts +++ b/modules/effects/spec/effects_metadata.spec.ts @@ -12,18 +12,18 @@ describe('Effects metadata', () => { @Effect({ dispatch: false }) c: any; d = createEffect(() => of({ type: 'a' }), { dispatch: false }); - @Effect({ dispatch: false, resubscribeOnError: false }) + @Effect({ dispatch: false, useEffectsErrorHandler: false }) e: any; z: any; } const mock = new Fixture(); const expected: EffectMetadata[] = [ - { propertyName: 'a', dispatch: true, resubscribeOnError: true }, - { propertyName: 'c', dispatch: false, resubscribeOnError: true }, - { propertyName: 'b', dispatch: true, resubscribeOnError: true }, - { propertyName: 'd', dispatch: false, resubscribeOnError: true }, - { propertyName: 'e', dispatch: false, resubscribeOnError: false }, + { propertyName: 'a', dispatch: true, useEffectsErrorHandler: true }, + { propertyName: 'c', dispatch: false, useEffectsErrorHandler: true }, + { propertyName: 'b', dispatch: true, useEffectsErrorHandler: true }, + { propertyName: 'd', dispatch: false, useEffectsErrorHandler: true }, + { propertyName: 'e', dispatch: false, useEffectsErrorHandler: false }, ]; expect(getSourceMetadata(mock)).toEqual( @@ -45,20 +45,20 @@ describe('Effects metadata', () => { e: any; f = createEffect(() => of({ type: 'f' }), { dispatch: false }); g = createEffect(() => of({ type: 'g' }), { - resubscribeOnError: false, + useEffectsErrorHandler: false, }); } const mock = new Fixture(); expect(getEffectsMetadata(mock)).toEqual({ - a: { dispatch: true, resubscribeOnError: true }, - c: { dispatch: true, resubscribeOnError: true }, - e: { dispatch: false, resubscribeOnError: true }, - b: { dispatch: true, resubscribeOnError: true }, - d: { dispatch: true, resubscribeOnError: true }, - f: { dispatch: false, resubscribeOnError: true }, - g: { dispatch: true, resubscribeOnError: false }, + a: { dispatch: true, useEffectsErrorHandler: true }, + c: { dispatch: true, useEffectsErrorHandler: true }, + e: { dispatch: false, useEffectsErrorHandler: true }, + b: { dispatch: true, useEffectsErrorHandler: true }, + d: { dispatch: true, useEffectsErrorHandler: true }, + f: { dispatch: false, useEffectsErrorHandler: true }, + g: { dispatch: true, useEffectsErrorHandler: false }, }); }); diff --git a/modules/effects/src/effect_creator.ts b/modules/effects/src/effect_creator.ts index 15cea4a320..7d972d41cb 100644 --- a/modules/effects/src/effect_creator.ts +++ b/modules/effects/src/effect_creator.ts @@ -15,7 +15,7 @@ type ObservableType = T extends false ? OriginalType : Action; * Creates an effect from an `Observable` and an `EffectConfig`. * * @param source A function which returns an `Observable`. - * @param config A `Partial` to configure the effect. By default, `dispatch` is true and `resubscribeOnError` is true. + * @param config A `Partial` to configure the effect. By default, `dispatch` is true and `useEffectsErrorHandler` is true. * @returns If `EffectConfig`#`dispatch` is true, returns `Observable`. Else, returns `Observable`. * * @usageNotes diff --git a/modules/effects/src/effect_sources.ts b/modules/effects/src/effect_sources.ts index 97796dfc45..cc21feb62e 100644 --- a/modules/effects/src/effect_sources.ts +++ b/modules/effects/src/effect_sources.ts @@ -1,4 +1,4 @@ -import { ErrorHandler, Injectable } from '@angular/core'; +import { ErrorHandler, Inject, Injectable, Optional } from '@angular/core'; import { Action, Store } from '@ngrx/store'; import { Notification, Observable, Subject } from 'rxjs'; import { @@ -14,18 +14,25 @@ import { reportInvalidActions, EffectNotification, } from './effect_notification'; -import { mergeEffects } from './effects_resolver'; +import { mergeEffects, EffectsErrorHandler } from './effects_resolver'; import { onIdentifyEffectsKey, onRunEffectsKey, OnRunEffects, onInitEffects, } from './lifecycle_hooks'; +import { EFFECTS_ERROR_HANDLER } from './tokens'; import { getSourceForInstance } from './utils'; @Injectable() export class EffectSources extends Subject { - constructor(private errorHandler: ErrorHandler, private store: Store) { + constructor( + private errorHandler: ErrorHandler, + private store: Store, + @Optional() + @Inject(EFFECTS_ERROR_HANDLER) + private effectsErrorHandler: EffectsErrorHandler | null + ) { super(); } @@ -49,7 +56,12 @@ export class EffectSources extends Subject { mergeMap(source$ => source$.pipe(groupBy(effectsInstance))), mergeMap(source$ => source$.pipe( - exhaustMap(resolveEffectSource(this.errorHandler)), + exhaustMap( + resolveEffectSource( + this.errorHandler, + this.effectsErrorHandler || undefined + ) + ), map(output => { reportInvalidActions(output, this.errorHandler); return output.notification; @@ -77,10 +89,15 @@ function effectsInstance(sourceInstance: any) { } function resolveEffectSource( - errorHandler: ErrorHandler + errorHandler: ErrorHandler, + effectsErrorHandler?: EffectsErrorHandler ): (sourceInstance: any) => Observable { return sourceInstance => { - const mergedEffects$ = mergeEffects(sourceInstance, errorHandler); + const mergedEffects$ = mergeEffects( + sourceInstance, + errorHandler, + effectsErrorHandler + ); if (isOnRunEffects(sourceInstance)) { return sourceInstance.ngrxOnRunEffects(mergedEffects$); diff --git a/modules/effects/src/effects_metadata.ts b/modules/effects/src/effects_metadata.ts index 292001fe6d..fa8f680027 100644 --- a/modules/effects/src/effects_metadata.ts +++ b/modules/effects/src/effects_metadata.ts @@ -6,9 +6,9 @@ export function getEffectsMetadata(instance: T): EffectsMetadata { return getSourceMetadata(instance).reduce( ( acc: EffectsMetadata, - { propertyName, dispatch, resubscribeOnError } + { propertyName, dispatch, useEffectsErrorHandler } ) => { - acc[propertyName] = { dispatch, resubscribeOnError }; + acc[propertyName] = { dispatch, useEffectsErrorHandler }; return acc; }, {} diff --git a/modules/effects/src/effects_module.ts b/modules/effects/src/effects_module.ts index 187f1dc753..f6762648a9 100644 --- a/modules/effects/src/effects_module.ts +++ b/modules/effects/src/effects_module.ts @@ -1,16 +1,16 @@ import { - NgModule, ModuleWithProviders, - Type, + NgModule, Optional, SkipSelf, + Type, } from '@angular/core'; -import { EffectSources } from './effect_sources'; import { Actions } from './actions'; -import { ROOT_EFFECTS, FEATURE_EFFECTS, _ROOT_EFFECTS_GUARD } from './tokens'; +import { EffectSources } from './effect_sources'; import { EffectsFeatureModule } from './effects_feature_module'; import { EffectsRootModule } from './effects_root_module'; import { EffectsRunner } from './effects_runner'; +import { _ROOT_EFFECTS_GUARD, FEATURE_EFFECTS, ROOT_EFFECTS } from './tokens'; @NgModule({}) export class EffectsModule { diff --git a/modules/effects/src/effects_resolver.ts b/modules/effects/src/effects_resolver.ts index 8a95d4bf4d..0e5ed96959 100644 --- a/modules/effects/src/effects_resolver.ts +++ b/modules/effects/src/effects_resolver.ts @@ -1,15 +1,27 @@ import { Action } from '@ngrx/store'; import { merge, Notification, Observable } from 'rxjs'; -import { ignoreElements, map, materialize, catchError } from 'rxjs/operators'; +import { + ignoreElements, + map, + materialize, + catchError, + tap, +} from 'rxjs/operators'; import { EffectNotification } from './effect_notification'; import { getSourceMetadata } from './effects_metadata'; import { getSourceForInstance } from './utils'; import { ErrorHandler } from '@angular/core'; +export type EffectsErrorHandler = ( + observable$: Observable, + errorHandler: ErrorHandler +) => Observable; + export function mergeEffects( sourceInstance: any, - errorHandler?: ErrorHandler + globalErrorHandler: ErrorHandler, + effectsErrorHandler: EffectsErrorHandler = resubscribeInCaseOfError ): Observable { const sourceName = getSourceForInstance(sourceInstance).constructor.name; @@ -17,22 +29,22 @@ export function mergeEffects( ({ propertyName, dispatch, - resubscribeOnError, + useEffectsErrorHandler, }): Observable => { const observable$: Observable = typeof sourceInstance[propertyName] === 'function' ? sourceInstance[propertyName]() : sourceInstance[propertyName]; - const resubscribable$ = resubscribeOnError - ? resubscribeInCaseOfError(observable$, errorHandler) + const effectAction$ = useEffectsErrorHandler + ? effectsErrorHandler(observable$, globalErrorHandler) : observable$; if (dispatch === false) { - return resubscribable$.pipe(ignoreElements()); + return effectAction$.pipe(ignoreElements()); } - const materialized$ = resubscribable$.pipe(materialize()); + const materialized$ = effectAction$.pipe(materialize()); return materialized$.pipe( map( diff --git a/modules/effects/src/index.ts b/modules/effects/src/index.ts index e871f62ea3..9334dacda5 100644 --- a/modules/effects/src/index.ts +++ b/modules/effects/src/index.ts @@ -2,7 +2,7 @@ export { createEffect } from './effect_creator'; export { EffectConfig } from './models'; export { Effect } from './effect_decorator'; export { getEffectsMetadata } from './effects_metadata'; -export { mergeEffects } from './effects_resolver'; +export { mergeEffects, EffectsErrorHandler } from './effects_resolver'; export { EffectsMetadata, CreateEffectMetadata } from './models'; export { Actions, ofType } from './actions'; export { EffectsModule } from './effects_module'; @@ -14,6 +14,7 @@ export { rootEffectsInit, EffectsRootModule, } from './effects_root_module'; +export { EFFECTS_ERROR_HANDLER } from './tokens'; export { act } from './act'; export { OnIdentifyEffects, diff --git a/modules/effects/src/models.ts b/modules/effects/src/models.ts index 5996ff6bd1..1d3aac91dc 100644 --- a/modules/effects/src/models.ts +++ b/modules/effects/src/models.ts @@ -10,12 +10,12 @@ export interface EffectConfig { /** * Determines if the effect will be resubscribed to if an error occurs in the main actions stream. */ - resubscribeOnError?: boolean; + useEffectsErrorHandler?: boolean; } export const DEFAULT_EFFECT_CONFIG: Readonly> = { dispatch: true, - resubscribeOnError: true, + useEffectsErrorHandler: true, }; export const CREATE_EFFECT_METADATA_KEY = '__@ngrx/effects_create__'; diff --git a/modules/effects/src/tokens.ts b/modules/effects/src/tokens.ts index de3923be1e..cc955571b9 100644 --- a/modules/effects/src/tokens.ts +++ b/modules/effects/src/tokens.ts @@ -1,4 +1,5 @@ import { InjectionToken, Type } from '@angular/core'; +import { EffectsErrorHandler } from './effects_resolver'; export const _ROOT_EFFECTS_GUARD = new InjectionToken( '@ngrx/effects Internal Root Guard' @@ -12,3 +13,6 @@ export const ROOT_EFFECTS = new InjectionToken[]>( export const FEATURE_EFFECTS = new InjectionToken( 'ngrx/effects: Feature Effects' ); +export const EFFECTS_ERROR_HANDLER = new InjectionToken( + 'ngrx/effects: Effects Error Handler' +); diff --git a/modules/store/testing/src/mock_reducer_manager.ts b/modules/store/testing/src/mock_reducer_manager.ts index b7fc1f6bb9..8c4431cf6c 100644 --- a/modules/store/testing/src/mock_reducer_manager.ts +++ b/modules/store/testing/src/mock_reducer_manager.ts @@ -9,7 +9,7 @@ export class MockReducerManager extends BehaviorSubject< constructor() { super(() => undefined); } - + addFeature(feature: any) { /* noop */ } diff --git a/projects/example-app/src/app/app.module.ts b/projects/example-app/src/app/app.module.ts index 6262bd8eb2..b9ad39081d 100644 --- a/projects/example-app/src/app/app.module.ts +++ b/projects/example-app/src/app/app.module.ts @@ -15,10 +15,7 @@ import { ROOT_REDUCERS, metaReducers } from '@example-app/reducers'; import { CoreModule } from '@example-app/core'; import { AppRoutingModule } from '@example-app/app-routing.module'; -import { - UserEffects, - RouterEffects -} from '@example-app/core/effects'; +import { UserEffects, RouterEffects } from '@example-app/core/effects'; import { AppComponent } from '@example-app/core/containers'; @NgModule({ diff --git a/projects/ngrx.io/content/guide/effects/lifecycle.md b/projects/ngrx.io/content/guide/effects/lifecycle.md index b17b132753..1e2bdb989b 100644 --- a/projects/ngrx.io/content/guide/effects/lifecycle.md +++ b/projects/ngrx.io/content/guide/effects/lifecycle.md @@ -52,7 +52,7 @@ In some cases where particular RxJS operators are used, the new behavior might produce unexpected results. For example, if the `startWith` operator is within the effect's pipe then it will be triggered again. -To disable resubscriptions add `{resubscribeOnError: false}` to the `createEffect` +To disable resubscriptions add `{useEffectsErrorHandler: false}` to the `createEffect` metadata (second argument). @@ -81,7 +81,7 @@ export class AuthEffects { ) // Errors are handled and it is safe to disable resubscription ), - { resubscribeOnError: false } + { useEffectsErrorHandler: false } ); constructor( @@ -91,6 +91,57 @@ export class AuthEffects { } +### Customizing the Effects Error Handler +Starting with version 9 the behavior of the default resubscription handler can be customized +by providing a custom handler using the `EFFECTS_ERROR_HANDLER` injection token. + +This will allow you to provide your own custom behavior such as only retrying on +certain "retryable" errors, or retrying a set number of times etc. + + +```ts +import { EffectsModule, EFFECTS_ERROR_HANDLER } from '@ngrx/effects'; +import { MovieEffects } from './effects/movie.effects'; +import { CustomErrorHandler, isRetryable } from '../custom-error-handler'; +import { Action } from '@ngrx/store'; +import { Observable, throwError } from 'rxjs'; +import { retryWhen, mergeMap } from 'rxjs/operators'; + +export function effectResubscriptionHandler( + observable$: Observable, + errorHandler?: CustomErrorHandler +): Observable { + return observable$.pipe( + retryWhen(errors => + errors.pipe( + mergeMap(e => { + if (isRetryable(e)) { + return errorHandler.handleRetryableError(e); + } + + errorHandler.handleError(e); + return throwError(e); + }) + ) + ) + ); +} + +@NgModule({ + imports: [EffectsModule.forRoot([MovieEffects])], + providers: [ + { + provide: EFFECTS_ERROR_HANDLER, + useValue: effectResubscriptionHandler, + }, + { + provide: ErrorHandle, + useClass: CustomErrorHandler + } + ], +}) + + ## Controlling Effects ### OnInitEffects