---
title: "Debouncing and race conditions"
date: "2026-02"
category: "Essays"
tags: ["Essays"]
description: "Debouncing matters, especially when there's network IO between frontend and backend. If every user action fires a request to the backend..."
source: "https://enriquemark.com/en/posts/%E9%98%B2%E6%8A%96%E5%92%8C%E7%AB%9E%E6%80%81%E9%97%AE%E9%A2%98"
---

Debouncing matters, especially when there's network IO between frontend and backend. If every user action fires a request to the backend, the bandwidth bill and the load on the backend server get heavy. The standard fix is to add a delay and only take the last of the user's consecutive actions in that window (or the final result).
On debouncing, Angular's RxJS library has operators that go with it:

```ts
// other imports omitted
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
@Component({ ... })
export class SearchComponent {
  // 1. Create a form control, which is itself an Observable source
  searchControl = new FormControl('');
  // 2. Define the result stream
  // If if an Observable is a water pipe, .pipe here can be understood as a filter
  // Things go in, get worked on a bunch, and at the end it outputs the processed result
  // valueChanges listens for value change events, and starts the work if there is one
  users$ = this.searchControl.valueChanges.pipe(
    // --- pipeline starts ---
    // note the three below are optional, their jobs overlap, no need to add them all
    
    // Debounce: if the user is still typing within 300ms, do nothing
    debounceTime(300),
    
    // Dedupe: if the user types "A" -> deletes it back to "" -> types "A" again,
    // the value hasn't changed, so don't send a request
    distinctUntilChanged(),
    
    // SwitchMap
    // Handles the race condition, as soon as a new value arrives it immediately cancels the last unfinished HTTP request
    // Only ever keeps the result of the last request
    switchMap(term => {
      // if empty, return an empty array, don't send a request
      if (!term) return []; 
      // fire a new request
      return this.http.get<User[]>(`/api/search?q=${term}`);
    })
  );

  constructor(private http: HttpClient) {}
}
```

A race condition, concretely, is when network jitter scrambles the load order of what the user sent, and the current request ends up overwritten by earlier data that came back later. `switchMap` is what you use to solve this. If the value changes, it cancels the previous request outright and only ever takes the result of the last one.

---

**Translation note.** I wrote this in Chinese. This English version is an LLM translation, so the wording is not mine even though the thinking is. Original: [防抖和竞态问题](</zh-hant/posts/防抖和竞态问题>).
