How Redux Prevents Nested Dispatches Using the isDispatching Flag

Redux uses an internal boolean flag called isDispatching to throw an error if code attempts to dispatch actions while a reducer is already executing, ensuring atomic and deterministic state updates.

Redux guarantees that only one reducer runs at a time by guarding its dispatch mechanism with an internal lock. This protection centers on the isDispatching flag defined in src/createStore.ts, which prevents nested and concurrent dispatches that could lead to inconsistent state or unpredictable side effects.

The isDispatching Flag Location and Purpose

Flag Definition in createStore.ts

According to the Redux source code, the flag is declared as a module-level variable:

let isDispatching = false;          // src/createStore.ts#L153

This boolean acts as a reentrancy guard that is initialized to false when the store is created and toggled exclusively within the dispatch function.

Why Redux Blocks Reentrancy

Redux reducers must be pure functions that take the current state and an action, then return new state without side effects. Allowing a reducer to trigger another dispatch would create nested execution contexts where the state is mid-update, violating the assumption that the state snapshot passed to the reducer is stable. The isDispatching flag enforces this single-execution guarantee.

How Redux Blocks Nested Dispatches in the dispatch Function

The dispatch implementation in src/createStore.ts wraps reducer execution in a strict check-and-lock pattern:

function dispatch(action: A) {
  // … validation logic …

  if (isDispatching) {
    throw new Error('Reducers may not dispatch actions.'); // src/createStore.ts#L3003‑L3005
  }

  try {
    isDispatching = true;                                 // src/createStore.ts#L3008
    currentState = currentReducer(currentState, action);
  } finally {
    isDispatching = false;                                // src/createStore.ts#L3011‑L3012
  }

  // … notify listeners …
}

Key mechanisms:

  • Pre-dispatch guard: Before the reducer runs, Redux checks isDispatching. If true, it throws immediately, preventing nested dispatches.
  • Atomic execution: The flag is set to true only after the check passes and cleared in a finally block, ensuring the lock releases even if the reducer throws an exception.
  • Single-thread safety: Because JavaScript is single-threaded, this boolean flag is sufficient to prevent concurrent dispatches without complex mutexes.

Protecting Other Store Methods During Dispatch

Redux extends the isDispatching guard beyond the dispatch method to maintain consistency across the store API.

Preventing getState Calls During Reducer Execution

The getState method refuses to return state while a reducer is active:

function getState(): S {
  if (isDispatching) {
    throw new Error(
      'You may not call store.getState() while the reducer is executing…' // src/createStore.ts#L177‑L182
    );
  }
  return currentState as S;
}

This prevents reducers from reading potentially inconsistent intermediate state.

Blocking Subscription Changes Mid-Dispatch

Both adding and removing listeners are prohibited during dispatch:

  • Subscribe guard: Lines 220‑227 in src/createStore.ts check isDispatching before registering a new listener.
  • Unsubscribe guard: Lines 240‑247 apply the same check when removing a listener.

These guards ensure the listener array remains stable while Redux iterates over it to notify subscribers after a state change.

Practical Code Examples

Illegal Nested Dispatch That Throws

Attempting to dispatch from within a reducer triggers the guard immediately:

import { createStore } from 'redux';

function counter(state = 0, action) {
  switch (action.type) {
    case 'inc':
      // ❌ illegal: tries to dispatch while reducer is running
      store.dispatch({ type: 'inc' });
      return state + 1;
    default:
      return state;
  }
}

const store = createStore(counter);
store.dispatch({ type: 'inc' });
// => Error: Reducers may not dispatch actions.

The error originates from the isDispatching check in dispatch (lines 3003‑3005).

Safe Dispatch from a Listener

Dispatching from a subscriber is valid because it occurs after the current dispatch completes:

import { createStore } from 'redux';

let ready = false;

function reducer(state = { value: 0 }, action) {
  switch (action.type) {
    case 'init':
      ready = true;
      return state;
    case 'inc':
      return { value: state.value + 1 };
    default:
      return state;
  }
}

const store = createStore(reducer);
store.subscribe(() => {
  if (ready) store.dispatch({ type: 'inc' }); // allowed: runs after dispatch finishes
});

store.dispatch({ type: 'init' });
console.log(store.getState()); // { value: 0 }
store.dispatch({ type: 'inc' });
console.log(store.getState()); // { value: 1 }

The listener runs after the current dispatch finishes, so isDispatching is false and the subsequent dispatch succeeds.

Preventing getState Inside a Reducer

Reading state mid-reduction is blocked to avoid inconsistent snapshots:

function badReducer(state = { count: 0 }, action) {
  if (action.type === 'inc') {
    // ❌ illegal: calling getState while reducer runs
    console.log(store.getState());
    return { count: state.count + 1 };
  }
  return state;
}

Calling store.getState() inside the reducer throws the error defined at lines 177‑182 of src/createStore.ts.

Summary

  • Redux guarantees single, atomic reducer execution using an internal isDispatching flag defined in src/createStore.ts.
  • The dispatch function checks isDispatching before invoking the reducer and throws "Reducers may not dispatch actions." if a nested dispatch is detected.
  • A try…finally block ensures isDispatching resets to false even if the reducer throws, preventing lock starvation.
  • getState, subscribe, and the unsubscribe function all share the same guard, throwing errors if called while a reducer is executing.
  • Because JavaScript is single‑threaded, this boolean flag is sufficient to prevent concurrent dispatches without complex synchronization primitives.

Frequently Asked Questions

What happens if a reducer tries to dispatch an action?

Redux throws an error with the message "Reducers may not dispatch actions." This occurs because the dispatch function in src/createStore.ts checks the isDispatching flag at lines 3003‑3005 before executing the reducer. If the flag is already true, the dispatch is rejected immediately to prevent nested execution.

Can I call getState inside a reducer?

No. Calling store.getState() while a reducer is executing throws an error: "You may not call store.getState() while the reducer is executing…" The getState implementation in src/createStore.ts (lines 177‑182) checks isDispatching and blocks the call to prevent reading inconsistent intermediate state.

Why does Redux allow dispatching from listeners but not reducers?

Dispatching from listeners is safe because listeners execute after the current dispatch cycle completes. By the time the subscriber array is notified, the isDispatching flag has been reset to false in the finally block (lines 3011‑3012). In contrast, reducers run while isDispatching is true, so attempting to dispatch from within a reducer would create nested execution, violating Redux's atomic update guarantee.

Is the isDispatching flag part of the public Redux API?

No. The isDispatching flag is an internal implementation detail of createStore.ts and is not exposed to consumers of the store. It exists solely as a runtime safeguard to enforce Redux's architectural constraints. You cannot access or modify this flag from application code; it is managed entirely within the Redux library's internal closure.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →