Fixing a Critical Race Condition Bug in a React Notification System
How I Discovered and Resolved a Production-Breaking Issue in Our Notification Infrastructure
When I encountered failing tests in our notification system, what seemed like a simple test fix turned into discovering a critical production bug that could have caused significant user frustration. This is the story of how systematic debugging led me to uncover and fix a race condition that would have deleted multiple notifications when users intended to dismiss just one.
The Initial Problem
I started with a test suite that was completely failing. Running the notification tests revealed a concerning situation:
npm test -- NotificationManager.test.tsx
All nine tests were failing with timeouts, and the console was flooded with React warnings about state updates not being wrapped in act()
. This wasn't just a test problem - it was a sign of deeper architectural issues.
Uncovering the Root Cause
Through systematic investigation, I discovered that our notification system was using timestamp-based IDs:
// The problematic code in uiSlice.ts
id: Date.now().toString()
This approach had a critical flaw. When multiple notifications were created within the same millisecond (which happens more often than you'd think in modern JavaScript execution), they would receive identical IDs. This meant that dismissing one notification could accidentally remove all notifications created at that exact moment.
The Investigation Process
To understand the full scope of the problem, I examined several key files:
# First, I checked the NotificationManager component
cat src/components/common/NotificationManager.tsx
# Then, I investigated the Redux slice managing notifications
cat src/store/slices/uiSlice.ts
# Finally, I reviewed the test implementation
cat src/components/common/__tests__/NotificationManager.test.tsx
This investigation revealed that the NotificationManager was correctly implemented, but the underlying ID generation in the Redux slice was fundamentally flawed.
The Solution
I implemented a robust ID generation system that combines three elements to guarantee uniqueness:
// src/utils/idGenerator.ts
let counter = 0;
export const generateUniqueId = (prefix: string = ''): string => {
// Increment counter and wrap around to prevent overflow
counter = (counter + 1) % 100000;
// Current timestamp in milliseconds
const timestamp = Date.now();
// Generate random string component
const random = Math.random().toString(36).substr(2, 5);
// Combine all components with hyphens for readability
return `${prefix}${timestamp}-${counter}-${random}`;
};
This approach ensures that even if thousands of notifications are created in the same millisecond, each will have a unique identifier through the combination of:
- Timestamp: Provides chronological ordering
- Counter: Ensures uniqueness within the same millisecond
- Random suffix: Adds additional entropy for distributed systems
Implementation and Verification
After creating the ID generator utility, I updated the Redux slice to use it:
// In src/store/slices/uiSlice.ts
import { generateUniqueId } from '../../utils/idGenerator';
// Inside the addNotification reducer
id: generateUniqueId('notif-'),
Running the tests again confirmed the fix:
npm test -- NotificationManager.test.tsx
# Result: All 9 tests passing in 271ms
✓ NotificationManager (9 tests) 271ms
Lessons Learned
This debugging journey reinforced several important principles:
Test failures often reveal production bugs: What started as fixing failing tests led to discovering a critical bug that would have affected real users.
Timestamp-based IDs are dangerous: In JavaScript's single-threaded but fast execution model, multiple operations within the same millisecond are common, not exceptional.
Systematic debugging pays off: By methodically examining each component and understanding the data flow, I could identify the root cause rather than just patching symptoms.
Simple solutions are often best: The fix required only a small utility function and a one-line change in the Redux slice, yet it eliminated a critical bug.
Technical Impact
This fix prevents a scenario where users could lose important notifications unintentionally. Consider a bulk operation that generates multiple success and error notifications - with the old system, dismissing one error could make all notifications disappear, leaving users confused about what succeeded and what failed.
Conclusion
What began as a routine test fix evolved into discovering and resolving a production-critical bug. This experience demonstrates the value of thorough testing and systematic debugging. By taking the time to understand why tests were failing rather than just making them pass, I was able to improve the reliability of our entire notification system.
The new ID generation system is now battle-tested and ensures that each notification can be independently managed, providing a better user experience and preventing data loss through accidental bulk deletions.
Keywords: React, Redux, JavaScript, Testing, Race Conditions, Debugging, Vitest, Material-UI
If you enjoyed this article, you can also find it published on LinkedIn and Medium.