DEENESH

NOTE–009 · Angular

Angular race conditions and predictable state

A screen that jumps backward after an edit may be showing exactly what its state contains. The fix starts with understanding which asynchronous updates are allowed to change that state.

I encountered this class of bug in a real-time Angular interface: an edit appeared to succeed, then the screen jumped back. My initial fix used detectChanges() to make the view reconcile. It reduced the visible symptom, but tracing the update paths showed that an incoming subscription value could still overwrite newer local state.

I moved configuration updates into a service, protected local changes while a save was active, and drove rendering through a shared reactive pipeline. I also stopped combining related display values from separate update paths, where one could lag behind the other. That removed the need for the manual change-detection call and addressed the flicker I had observed.

The experience changed how I debug asynchronous interfaces: first identify who can write to state, then establish when each update is valid. The task-board example below is illustrative, and the code is written for this article. The revision checks and reconciliation strategies describe how I would extend the approach for stronger concurrency guarantees.

Consider a collaborative task board. A user changes a task, the interface updates immediately, and a save starts in the background. Before that save settles, the user makes another edit. Then a real-time subscription delivers the state associated with the earlier change, and the latest edit disappears.

The visible symptom is flicker. The underlying problem is that an incoming update was allowed to overwrite newer local work.

JavaScript can experience this kind of race even though normal application code executes on a single main thread. User actions, HTTP responses, and subscription events interleave. Correctness depends on how the application handles their order.

Initial server state                     A
User edits; first save begins            B
User makes another local edit            C
Subscription delivers the earlier B      B ← newer edit disappears

The subscription does not have to deliver its own messages out of order. A valid server snapshot can still be older than the user’s pending work.

Inspect state before forcing a refresh

My first debugging question is whether the data is wrong or whether Angular has not rendered the correct data yet.

  • If component state is correct but the screen is stale, inspect bindings and change-detection notifications.
  • If component state has reverted, inspect every code path that writes to it.
  • If individual values look correct but their combination is inconsistent, check whether they belong to different revisions.
  • If an older request replaces a newer result, inspect request ordering and cancellation.

Calling detectChanges() checks a view and its children. It cannot decide which server response should win or recover an overwritten edit. It is a legitimate API for explicit local change detection, but it does not repair incorrect state. Angular ChangeDetectorRef documentation

Give state updates one owner

A component becomes difficult to reason about when user actions, HTTP responses, parent inputs, and real-time subscriptions can all independently replace the same state.

I prefer to route those events through a service, store, or reducer that owns the update rules. That owner decides whether to accept an update, reject it as stale, queue an operation, or reconcile it with pending local work. The component renders a view model derived from the resulting state.

User actions + server events
             ↓
Accept, reject, queue, or reconcile
             ↓
Consistent application state
             ↓
Derived view model
             ↓
Angular template

A BehaviorSubject, NgRx store, or signal can hold that state. None of these containers prevents races by itself. The rules governing updates provide correctness.

Suppose a view needs both task configuration and task details. If those values arrive through separate paths, rendering can briefly combine configuration from revision 12 with details from revision 11.

combineLatest combines the latest emitted values from its inputs. It does not guarantee that those values belong to the same server revision. A single reactive render pipeline makes dependencies clearer, but it does not turn independent emissions into an atomic update. RxJS combineLatest reference

When related values must remain consistent, I prefer to validate and reconcile a complete snapshot before publishing it. For example, a snapshot might contain a server revision, configuration, and task details together. If the backend provides them independently, the application needs a revision-matching or reconciliation policy before treating them as one snapshot.

The template can consume the derived observable through the async pipe. It marks the component for checking when a value arrives and unsubscribes when the component is destroyed. A manual subscription that assigns a normal field does not automatically provide that same notification. Angular AsyncPipe documentation

Treat a save guard as a limited mitigation

A basic protection is to avoid replacing local state while a save is in progress:

if (!savingInProgress) {
  applyRemoteSnapshot(snapshot);
}

That can protect an edit during a single save, but several cases remain:

  • A delayed snapshot can arrive after the flag becomes false.
  • Overlapping saves can reset the same flag incorrectly.
  • A legitimate remote change can be skipped permanently.
  • Another user can still overwrite the server state.

For a simple editor, allowing one save at a time and reconciling with the server afterward may be sufficient. For overlapping edits, track pending operations explicitly and define how incoming snapshots interact with them.

A guard controls a time window. It does not establish which data is newer.

For a server that supplies monotonically increasing revisions, the acceptance decision can be made explicit. This standalone TypeScript example assumes complete snapshots for the same entity and tracks the last confirmed server revision separately from local edits:

type SnapshotDecision = 'ignore' | 'reconcile' | 'apply';

function decideSnapshot(
  incomingRevision: number,
  confirmedRevision: number,
  pendingOperationCount: number
): SnapshotDecision {
  if (incomingRevision <= confirmedRevision) {
    return 'ignore';
  }

  return pendingOperationCount > 0 ? 'reconcile' : 'apply';
}

If revision 12 is confirmed, revision 11 is ignored. Revision 13 can be applied directly when no local work is pending. If an edit is pending, revision 13 goes through reconciliation so that replacing the displayed state does not silently discard that edit.

Here, reconcile is a decision, not an implemented merge algorithm. Its handler must account for which pending operations the snapshot already includes, preserve or reject remaining edits, and advance the confirmed revision. Save acknowledgements must still be processed even when a duplicate snapshot is ignored. This check rejects stale snapshots; it does not prevent conflicting server writes.

Choose the operator that matches the interaction

RxJS operators express different policies for overlapping work:

  • switchMap unsubscribes from the previous inner observable. It fits searches and filters where only the latest result matters.
  • concatMap queues operations and runs them sequentially. It fits saves where every action must be preserved.
  • exhaustMap ignores new actions while an operation is active. It can prevent duplicate submissions when ignoring those actions is intentional.
  • mergeMap allows concurrent work. It fits independent operations whose completion order does not matter.

See the RxJS references for switchMap, concatMap, and exhaustMap.

For a queue of saves, error handling belongs inside the individual save observable when later actions should continue after a failure. Otherwise, an unhandled error can terminate the outer stream and stop subsequent saves.

There are also limits to what an operator can guarantee. Unsubscribing with switchMap does not undo a server mutation that the backend has already received. concatMap orders this client’s requests, but it does not coordinate other clients. Queuing stale full snapshots can still overwrite newer data, even when requests run sequentially.

I choose the interaction policy first, then the operator that expresses it.

Make rollback aware of later edits

Optimistic updates make an interface responsive by displaying a change before the server confirms it. A common rollback strategy saves the original state and restores it if the request fails.

That is only safe when restoring the original state cannot erase later work. Consider this sequence:

  1. Edit A starts.
  2. Edit B starts.
  3. Edit B succeeds.
  4. Edit A fails.
  5. Restoring the state from before A erases B.

For more complex editing, a useful model separates confirmed server state from pending local operations. Displayed state is calculated by applying those pending operations to the confirmed state.

When an operation fails, remove that operation and rebuild the display using the latest confirmed state and remaining pending work. Operations that depend on the failed change may need to be revised or rejected as well.

Rollback is another state update. It needs the same ordering discipline as a successful response.

Protect concurrent writes on the server

Frontend coordination cannot guarantee correctness across multiple users. One common backend approach is optimistic concurrency control:

  1. The client reads revision 20.
  2. It submits a change with an expected revision of 20.
  3. The server atomically checks the revision and applies the write if it still matches.
  4. If the revision differs, the server returns a conflict.
  5. The client refreshes, merges, or asks the user to resolve the conflict.

The revision check and write must be atomic. A separate read followed by an unconditional write leaves another race window.

Database transactions are another option for read-modify-write operations. For example, Firebase Realtime Database transactions can retry against updated data after concurrent modifications. Database transaction documentation

An edit lock can reduce collisions, but reliable locks need atomic acquisition, ownership checks, expiry, and server enforcement. A local saving flag is not a distributed lock.

Client-side ordering protects the local interaction. Server-side concurrency control protects shared data.

Test the event order that causes the bug

Happy-path testing often misses races because requests finish in the expected order. I want tests that deliberately control the sequence:

  • An earlier snapshot arrives after a newer local edit.
  • Two saves finish in reverse order.
  • One save fails after another succeeds.
  • A remote update arrives during a local save.
  • Related values arrive from different revisions.
  • The client reconnects with pending changes.

Assertions should check the resulting state, not just the absence of flicker. RxJS marble tests help verify stream ordering, while integration tests are needed to verify server conflict behavior.

For diagnosis, log an entity identifier, operation identifier, event source, server revision, and pending-operation count. Timestamps help reconstruct a timeline, but server revisions are a stronger basis for deciding freshness than client clocks.

The lesson I keep

Predictable rendering starts with predictable state transitions. Before accepting an asynchronous update, the application should be able to explain what it represents, how it relates to pending work, and why it is allowed to replace what the user currently sees.