NgRx Effects stream termination after unhandled API errors
0 reputation · 07 Dec 2023, 19:40 UTC
0 reputation · 07 Dec 2023, 19:40 UTC
In NgRx applications using createEffect, asynchronous streams are designed to listen for specific actions and trigger external API calls. However, the behavior of the RxJS observable stream is critical; if an error occurs within the inner observable and is not caught, the outer stream may terminate.
When an Effect stream completes or errors out, it stops listening for subsequent actions, effectively disabling that piece of business logic until the application is reloaded. This creates a challenge when balancing global error handling with the need to keep the Effect active for future dispatches.
Where should the error handling logic be placed to prevent the main Effect stream from terminating? What is the recommended pattern for recovering the stream without introducing memory leaks?
31275 reputation · 08 Dec 2023, 02:07 UTC
Place the error‑handling operator inside the mergeMap (or concatMap/switchMap) that performs the API call. The catchError must be applied to the inner observable, not to the outer actions$ stream. Returning a new observable (e.g., of(failureAction) or EMPTY) keeps the effect’s source observable alive, so subsequent actions are still processed.
An uncaught error in the inner observable propagates to the outer stream, causing the effect to complete. By catching the error inside the inner pipeline and emitting a value (or nothing), the outer stream receives no completion notification and remains subscribed.
Use mergeMap (or concatMap if ordering matters) to call the API:
createEffect(() =>
this.actions$.pipe(
ofType(loadData),
mergeMap(action =>
this.api.fetch(action.id).pipe(
map(data => loadDataSuccess({ data })),
catchError(err =>
// Log, dispatch a failure action, or silently swallow
of(loadDataFailure({ error: err }))
)
)
)
)
);
If you need retry logic, place retry (or retryWhen) before catchError:
mergeMap(action =>
this.api.fetch(action.id).pipe(
retry(2),
map(...),
catchError(err => of(loadDataFailure({ error: err })))
)
)
Never subscribe manually inside the effect. Manual subscriptions create a separate observable chain that can leak if not unsubscribed.
Do not rely on a global error handler that only logs; it must re‑emit an observable so the effect stays alive.
Unit‑test the flow: dispatch the action, mock the service to throw, assert a failure action is emitted, then dispatch again and confirm the effect still runs.
Does your effect currently use switchMap instead of mergeMap or concatMap? If so, the same pattern applies, but you must be careful that switchMap cancels previous requests – this can affect how you want to recover from errors.
of or EMPTY) to keep the effect alive.Use comments to ask for clarification. Post a solution as an answer.
31,275 reputation · 08 Dec 2023, 06:11 UTC
One clarification worth adding to the answer above: the placement of catchError is only half the rule — what it returns matters just as much. A catchError that logs and then rethrows (or returns throwError) still propagates the error to the outer stream and terminates the effect. It must return a replacement observable — typically of(failureAction) or EMPTY if you intentionally want silence.
Also note that some NgRx versions offer automatic resubscription behavior (e.g., useEffectsErrorHandler customization), but this is version-sensitive, so the explicit inner catchError remains the portable approach. A quick verification: dispatch the action twice in a marble test, fail the first HTTP call, and assert the second dispatch still emits — that proves the stream survived.