NgRx Effect Never Fires: A Diagnostic Guide for Silent Action Dispatch Failures
Dispatched an NgRx action and nothing happened? A fixed diagnostic order isolates the five real causes: registration scope, action mismatches, dead effect streams, wrong flattening operators, and missing reducer handling.
01 Feb 2026, 20:47 UTC

You dispatch an action, and nothing happens. No HTTP request, no follow-up action, no state change. Redux DevTools might show the action sitting there with no response, or the action might not appear at all. This is one of the most common NgRx failure modes, and the fix is almost always one of five causes: registration, dispatch mismatches, a dead effect stream, the wrong flattening operator, or a misconfigured effect output.
The good news: you can isolate the cause in a fixed order, and each check points to a specific fix. This guide assumes a recent NgRx version (15+) using createEffect or functional effects with provideEffects. If you are on an older version with class-based @Effect() decorators, the same causes apply but the registration syntax differs — check your installed version before applying fixes.
Cause and symptom table
| Symptom | Likely cause |
|---|---|
| Action never appears in DevTools | Dispatch-side problem: wrong action creator, typo in the type string, or the dispatch line never executes |
| Action appears, effect never runs, ever | Effects not registered in this scope, or ofType does not match the action type |
| Effect runs once, then never again | Uncaught error in the inner observable completed the effect stream |
| Effect runs but no state change | { dispatch: false } set unintentionally, or the reducer does not handle the success action |
| Effect runs only sometimes, or out of order | Wrong flattening operator (mergeMap vs concatMap/exhaustMap) |
| Effect works eagerly but not on lazy-loaded routes | Feature effects registered only in the root module, not in the lazy feature scope |
Check 1: Does the action appear in Redux DevTools?
With @ngrx/store-devtools enabled, open the Redux DevTools extension and trigger the flow. If the action does not appear, the problem is upstream of the store: the dispatch line never runs, or the component dispatches a different action creator than you think.
With createActionGroup, a frequent slip is a mismatched event name between the dispatch site and the effect. The generated type string is built from the source and event names, so '[Orders Page] Load' and '[Orders API] Load' are different actions even if they look interchangeable in your head. Compare the exact action creator imported at the dispatch site with the one inside ofType() — do not trust the type strings by eye.
Check 2: Are the effects registered in the right scope?
If the action appears but the effect never runs, registration is the first suspect. Feature effects must be registered in the same scope as the feature state. In an NgModule-based app:
@NgModule({
imports: [
StoreModule.forFeature(ordersFeature),
EffectsModule.forFeature([OrdersEffects]),
],
})
export class OrdersModule {}In a standalone app using route-level providers:
export const ordersRoutes: Routes = [
{
path: 'orders',
providers: [
provideState(ordersFeature),
provideEffects(OrdersEffects),
],
loadComponent: () => import('./orders.page'),
},
];Registering effects only in AppModule (or only at the root bootstrapApplication) works for root effects but not for lazy features. Also do not mix styles inconsistently for the same feature — for example, provideState at the route but EffectsModule.forFeature in a module that never loads. One more timing trap: actions dispatched before a lazy feature loads are not replayed to effects registered later, so an early dispatch can look exactly like a registration bug. Check when the dispatch happens relative to route activation.
Check 3: Does the action reach the effect stream?
Add a temporary tap before ofType to confirm the stream receives the action at all:
loadOrders$ = createEffect(() =>
this.actions$.pipe(
tap(a => console.log('saw action', a.type)), // temporary
ofType(OrdersPageActions.load),
switchMap(() =>
this.ordersService.getAll().pipe(
map(orders => OrdersApiActions.loadSuccess({ orders })),
catchError(err => of(OrdersApiActions.loadFailure({ error: err.message })))
)
)
)
);If saw action logs but the effect body never runs, ofType is filtering the action out — go back to Check 1 and compare action creators. If nothing logs at all, the effect class is not registered (Check 2) or the Actions stream was never injected. Remove the tap once diagnosed; logging every action in production is noisy.
Check 4: Did the effect stream die on the first error?
This is the classic "works once, then never again" pattern. In RxJS, when an observable errors, the stream completes. If an HTTP call inside your effect throws and there is no catchError — or the catchError is on the outer stream instead of inside the inner observable — the entire effect stream terminates permanently. Subsequent dispatched actions are simply never processed.
The diagnostic: trigger the flow once (it works), then dispatch the same action again. If the effect stays silent on the second attempt, you have a dead stream. The fix is placing catchError inside the inner observable, after the HTTP call, as shown in the example above. It must return a recovery observable (typically of(...) with a failure action), not rethrow.
Check 5: Is the effect configured to emit, and does the reducer listen?
Two final possibilities when the effect clearly executes but nothing changes:
{ dispatch: false }set unintentionally. This option is meant for effects that only perform side effects (like logging or navigation). If it is set on an effect that returns a success action, the effect runs but the output action is never dispatched. Check the second argument tocreateEffect.- The reducer does not handle the success action, or the feature slice is not registered. Confirm the reducer has an
on(...)for the exact action the effect emits, and that the feature state slice is registered in the same scope as the effects. In DevTools, if the success action appears but the state tree does not change, the reducer is the gap.
If it fires "sometimes": flattening operators
Intermittent or out-of-order behavior usually means the wrong flattening operator. mergeMap runs inner requests concurrently, so responses can arrive out of order — the last dispatch does not necessarily win. switchMap cancels in-flight requests, which can drop work you expected to complete. For load-once flows like form submissions, exhaustMap (ignore new actions while busy) or concatMap (queue them) is usually the intent. If users report "sometimes my save doesn't happen," check for switchMap cancelling a save when a second action arrives.
Escalation criteria
Move beyond manual inspection when any of these hold:
- You have confirmed registration, dispatch, and
catchErrorplacement, and the effect still behaves inconsistently — write a unit test withprovideMockActionsto replay the exact action sequence and assert the output action. This isolates the effect from the rest of the app. - The failure only happens under specific timing (e.g., dispatch during route transition). Reproduce it in a test with a mocked
Actionsstream rather than chasing it in the browser. - Multiple effects compete over the same action type and you cannot tell which one misbehaves — temporarily give each effect a distinct
taplabel, or split the flow into separate action types.
Verifying the fix
After each fix, verify in this order: (1) the dispatched action appears in DevTools; (2) the effect's success or failure action follows it; (3) the state slice updates. Then dispatch the action a second and third time — this specifically rules out the dead-stream cause, which a single successful run cannot. Finally, run the failing path with a forced HTTP error (e.g., block the request in browser dev tools) to confirm the failure action is emitted and the effect still fires afterward.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.