LogoTRUONG PHAM
Home
Projects
Blogs
YouTube
Contact

Newsletter

Stay updated with technical artifacts and engineering insights.

LogoTRUONG PHAM

Building scalable software and sharing insights on technology & life.

Sitemap

  • Home
  • Projects
  • Blogs
  • YouTube
  • Contact

Connect

  • GitHub
  • LinkedIn
  • Email
  • YouTube

© 2024 TRUONG PHAM. © All rights reserved.

Privacy PolicyTerms of Service
Back
Blog #5: 200 OK – The system's sweet little lies
50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

Blog #5: 200 OK – The system's sweet little lies

The story of times when the Network Tab was perfectly green but the UI showed something else entirely, and the lesson of managing asynchronous state in the real world.

TP
Truong PhamSoftware Engineer
PublishedMarch 22, 2024

There was a naive belief I held for many years before finally letting go: believing that as long as the Network Tab shows a green 200 OK, everything is fine.

I remember once implementing a "Search-as-you-type" feature for a large-scale human resources management project. When I typed "Nam", the correct results appeared. But when I quickly typed "Nguyen Nam", something strange happened: the interface displayed a list of people named "Nam" (the result of the first keyword), even though the search bar clearly said "Nguyen Nam".

Checked the Network? Everything was perfect. Both requests returned 200 OK with accurate data. So why did the UI dare to "betray" the data like that?

1. The Silent Killer: Race Condition

In Frontend development, we live in an uncertain world. When you send off 2 requests in order A then B, there is no physical law guaranteeing B will return after A.

If Request A hits a bottleneck at the database and takes 2 seconds to respond, while Request B runs through the cache and returns in just 0.5 seconds, then the result of A (which is now old) will overwrite the result of B. This is the Race Condition—one of the most annoying and hard-to-reproduce errors.

2. The Problem: When data "steps" on each other

My system at the time handled extremely complex search filters. Users could select department, position, and type a name at the same time. Every time something changed, a new request was sent off.

// My naive logic at the time
const [searchResult, setSearchResult] = useState([]);

const onSearch = async (query) => {
  const data = await fetchSearchAPI(query);
  setSearchResult(data); // The "trap" is here
};

Suppose a user types "A" (Request 1), then deletes it and types "B" (Request 2). If Request 1 is slower, after Request 2 has already finished displaying results for "B", Request 1 finishes and calls setSearchResult with data for "A". The user sees "B" in the search bar but the results are for "A". A perfect mismatch.

3. The Process: Searching for the missing key

Initially, I thought the error was logic at the Backend. I forced the Backend team to log the request and response times. Result: the Backend was completely innocent. They handled everything in the right order with the right data.

I started to "question life" and thought about using a loading flag. But loading only helps show a spinner—it doesn't solve old data overwriting new data.

The lifesaver for me then was AbortController. I realized I needed a way to "terminate" old requests as soon as a new request was initialized.

The final fix:

const abortControllerRef = useRef(null);

const onSearch = async (query) => {
  // If a request is already running, cancel it!
  if (abortControllerRef.current) {
    abortControllerRef.current.abort();
  }

  abortControllerRef.current = new AbortController();

  try {
    const data = await fetchSearchAPI(query, { 
      signal: abortControllerRef.current.signal 
    });
    setSearchResult(data);
  } catch (error) {
    if (error.name === 'AbortError') return; // Ignore if we intentionally canceled it
    // Handle other real errors...
  }
};

4. Trade-offs: Consistency vs. Performance

Using AbortController helps the UI always remain consistent with the user's final input. However, it also has a downside: you're wasting server resources if users type/delete continuously. The server still has to process that request until it detects the connection has been closed.

If I were to do it again, I would combine Debounce (wait for the user to stop typing for 300ms before sending a request) with AbortController. This combination brings a perfect balance between user experience and system performance.

5. Junior vs. Senior: Conditional Trust

A Junior developer often believes: "If the set function is called last in the code, it will update the final value." They forget the Time factor.

A Senior developer approaches with "State Invalidation". They always ask: "If this data comes back slower than the data after it, what will happen?". They don't just manage data—they manage the lifecycle of that data.

Nowadays, with tools like React Query or SWR, this issue has been resolved "under the hood." But understanding the nature of Race Conditions will help you handle cases where libraries aren't used, such as handling WebSockets or other complex side effects.

6. Lessons Learned

After that "Out of Sync Search" incident, I learned two major lessons:

  1. Asynchronous data is untrustworthy: never assume the return order of API calls.
  2. Cleanliness in UI starts from cleanliness in Requests: always clean up old effects or requests before starting new ones.

If you find your UI is "mismatched," don't rush to check the API body. Check if you're living in a race where you're the one losing.


In the Async world, the one who finishes first isn't necessarily the winner.

Series • Part 5 of 50

50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

NextBlog #6: Why you shouldn't fully trust console.log – The silent deceiver
Blog #4: Hydration Error – When SSR's perfection 'slaps' me in the face at midnight
01Blog #1: When 'It works on my machine' is the sweetest lie02Blog #2: The 60 FPS Illusion – When your MacBook Pro 'deceives' your senses03Blog #3: Infinite Loops – When useEffect turns the browser into a 'furnace'04Blog #4: Hydration Error – When SSR's perfection 'slaps' me in the face at midnight05Blog #5: 200 OK – The system's sweet little liesReading06Blog #6: Why you shouldn't fully trust console.log – The silent deceiver07Blog #7: The 'Double' Nightmare – When React StrictMode tests your patience08Blog #8: Memory Trash – The price of forgetting to 'clean up' your Event Listeners09Blog #9: Safari – The new 'Internet Explorer' of the modern era?10Blog #10: Specificity War – When !important marks the start of a civil war
TP

Written by Truong Pham

Software Engineer passionate about building high-performance systems and meaningful experiences.

Read more articles