📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,398 @@
|
||||
# Race Condition Debugging Session
|
||||
|
||||
This example demonstrates debugging an intermittent issue using the four-stage framework.
|
||||
|
||||
## The Bug
|
||||
|
||||
User reports: "Cache occasionally returns stale data. Works most of the time, but sometimes shows old values after updates."
|
||||
|
||||
## Stage 1: Collect Evidence
|
||||
|
||||
**Task state**: "Collecting evidence" (in_progress)
|
||||
|
||||
### Read Error Description
|
||||
|
||||
No error messages — behavior is wrong but silent:
|
||||
- Expected: Updated value from cache
|
||||
- Actual: Old value returned (intermittently)
|
||||
- Frequency: ~5% of requests after update
|
||||
|
||||
### Reproduce Consistently
|
||||
|
||||
Initial attempts fail — bug is intermittent:
|
||||
- Sometimes works correctly
|
||||
- Sometimes returns stale data
|
||||
- No obvious pattern
|
||||
|
||||
Run test 100 times to find pattern:
|
||||
|
||||
```bash
|
||||
for i in {1..100}; do
|
||||
npm test -- --grep "cache update" > /dev/null || echo "Failed: $i"
|
||||
done
|
||||
|
||||
# Results:
|
||||
# Failed: 3
|
||||
# Failed: 17
|
||||
# Failed: 28
|
||||
# Failed: 41
|
||||
# Failed: 59
|
||||
# Failed: 72
|
||||
# Failed: 88
|
||||
# Failed: 94
|
||||
```
|
||||
|
||||
Failure rate: ~8% (8 failures out of 100 runs)
|
||||
|
||||
### Check Recent Changes
|
||||
|
||||
```bash
|
||||
git log --since="1 week ago" --oneline src/cache/
|
||||
|
||||
# f8d3c21 Optimize cache reads with async/await
|
||||
# 9a2b741 Add cache prewarming on startup
|
||||
# c4e5d67 Implement cache TTL refresh
|
||||
```
|
||||
|
||||
Recent changes to cache implementation — possible cause.
|
||||
|
||||
### Gather Evidence with Timestamps
|
||||
|
||||
Add detailed timing logs:
|
||||
|
||||
```typescript
|
||||
export async function updateCache(key: string, value: any): Promise<void> {
|
||||
console.log(`[${Date.now()}] updateCache START: ${key}`);
|
||||
|
||||
await cache.set(key, value);
|
||||
console.log(`[${Date.now()}] updateCache cache.set COMPLETE: ${key}`);
|
||||
|
||||
await invalidateRelated(key);
|
||||
console.log(`[${Date.now()}] updateCache invalidateRelated COMPLETE: ${key}`);
|
||||
}
|
||||
|
||||
export async function getCache(key: string): Promise<any> {
|
||||
console.log(`[${Date.now()}] getCache START: ${key}`);
|
||||
|
||||
const value = await cache.get(key);
|
||||
console.log(`[${Date.now()}] getCache COMPLETE: ${key}`, value);
|
||||
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
### Timeline Analysis
|
||||
|
||||
Captured logs from a failure:
|
||||
|
||||
```
|
||||
[1702500123450] updateCache START: user:123
|
||||
[1702500123455] updateCache cache.set COMPLETE: user:123
|
||||
[1702500123456] getCache START: user:123 <-- Read started
|
||||
[1702500123458] getCache COMPLETE: user:123 [OLD] <-- Returned old value
|
||||
[1702500123460] updateCache invalidateRelated COMPLETE: user:123
|
||||
```
|
||||
|
||||
**Key finding**: `getCache` started (1456) AFTER `cache.set` completed (1455) but BEFORE `invalidateRelated` completed (1460). Returned stale data.
|
||||
|
||||
**Transition**: Evidence gathered showing timing issue. Mark "Collect Evidence" complete, add "Isolate Variables" as in_progress.
|
||||
|
||||
## Stage 2: Isolate Variables
|
||||
|
||||
**Task state**: "Isolating variables" (in_progress)
|
||||
|
||||
### Find Working Examples
|
||||
|
||||
Check how other cache operations handle this:
|
||||
|
||||
```typescript
|
||||
// Working example - authentication cache
|
||||
export async function updateAuthCache(userId: string, token: string): Promise<void> {
|
||||
const key = `auth:${userId}`;
|
||||
|
||||
// Atomic operation - no race window
|
||||
await cache.set(key, token, { ttl: 3600 });
|
||||
}
|
||||
```
|
||||
|
||||
Difference: No separate invalidation step, single atomic operation.
|
||||
|
||||
### Read Recent Optimization Commit
|
||||
|
||||
The optimization commit (f8d3c21):
|
||||
|
||||
**Before** (synchronous, blocking):
|
||||
|
||||
```typescript
|
||||
export function updateCache(key: string, value: any): void {
|
||||
cache.set(key, value); // Synchronous
|
||||
invalidateRelated(key); // Synchronous
|
||||
// Both complete before function returns
|
||||
}
|
||||
```
|
||||
|
||||
**After** (async, non-blocking):
|
||||
|
||||
```typescript
|
||||
export async function updateCache(key: string, value: any): Promise<void> {
|
||||
await cache.set(key, value); // Async - completes
|
||||
await invalidateRelated(key); // Async - still pending
|
||||
// Function returns here, but invalidation still running
|
||||
}
|
||||
```
|
||||
|
||||
### Identify Differences
|
||||
|
||||
Working code:
|
||||
- Single atomic operation
|
||||
- No race window
|
||||
- Consistent state
|
||||
|
||||
Broken code:
|
||||
- Two-step process
|
||||
- Race window between set and invalidate
|
||||
- Inconsistent state possible
|
||||
|
||||
### Understand the Flow
|
||||
|
||||
1. `updateCache('user:123', newData)` starts
|
||||
2. `cache.set` completes — new data in cache
|
||||
3. **Race window starts**
|
||||
4. `getCache('user:123')` called from different request
|
||||
5. Reads from cache — gets new data
|
||||
6. BUT cache has stale related entries
|
||||
7. Related entries override with old data
|
||||
8. **Race window ends**
|
||||
9. `invalidateRelated` completes
|
||||
|
||||
**Root cause hypothesis forming**: The async optimization introduced a race window between setting the value and invalidating related entries.
|
||||
|
||||
**Transition**: Pattern identified. Mark "Isolate Variables" complete, add "Formulate Hypotheses" as in_progress.
|
||||
|
||||
## Stage 3: Formulate Hypotheses & Test
|
||||
|
||||
**Task state**: "Formulating hypotheses" (in_progress)
|
||||
|
||||
### Form Hypothesis
|
||||
|
||||
**Hypothesis**: "Cache returns stale data because the async optimization (f8d3c21) introduced a race window. When `updateCache` sets a value but hasn't yet invalidated related entries, concurrent `getCache` calls can read the new value while related entries are still stale, causing those stale entries to be returned instead."
|
||||
|
||||
Evidence:
|
||||
- Timeline shows `getCache` called during race window
|
||||
- Worked before async optimization
|
||||
- Fails ~8% of time (when timing hits race window)
|
||||
- Working code uses atomic operations
|
||||
|
||||
**Transition**: Hypothesis formed. Mark "Formulate Hypotheses" complete, add "Test Hypothesis" as in_progress.
|
||||
|
||||
**Task state**: "Testing hypothesis" (in_progress)
|
||||
|
||||
### Design Minimal Test
|
||||
|
||||
Add artificial delay to widen race window and make bug consistent:
|
||||
|
||||
```typescript
|
||||
export async function updateCache(key: string, value: any): Promise<void> {
|
||||
await cache.set(key, value);
|
||||
|
||||
// TESTING: Widen race window
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
await invalidateRelated(key);
|
||||
}
|
||||
```
|
||||
|
||||
### Execute Test
|
||||
|
||||
Run test 100 times with widened race window:
|
||||
|
||||
```bash
|
||||
for i in {1..100}; do
|
||||
npm test -- --grep "cache update" > /dev/null || echo "Failed: $i"
|
||||
done
|
||||
|
||||
# Results: 67 failures (67%)
|
||||
```
|
||||
|
||||
Failure rate increased dramatically with wider race window. Confirms timing-based hypothesis.
|
||||
|
||||
### Test Solution
|
||||
|
||||
Make operations atomic by ensuring no reads during update:
|
||||
|
||||
```typescript
|
||||
export async function updateCache(key: string, value: any): Promise<void> {
|
||||
// Acquire lock to prevent concurrent reads
|
||||
const lock = await cache.lock(key);
|
||||
|
||||
try {
|
||||
await cache.set(key, value);
|
||||
await invalidateRelated(key);
|
||||
} finally {
|
||||
await lock.release();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Run 100 times:
|
||||
|
||||
```bash
|
||||
# Results: 0 failures (0%)
|
||||
```
|
||||
|
||||
**Result**: Hypothesis confirmed. Lock prevents race condition.
|
||||
|
||||
**Transition**: Solution verified. Mark "Test Hypothesis" complete, add "Verify Fix" as in_progress.
|
||||
|
||||
## Stage 4: Verify Fix
|
||||
|
||||
**Task state**: "Verifying fix" (in_progress)
|
||||
|
||||
### Create Failing Test
|
||||
|
||||
```typescript
|
||||
describe('updateCache race condition', () => {
|
||||
it('prevents stale data during concurrent update and read', async () => {
|
||||
// Setup initial data
|
||||
await cache.set('user:123', 'old-value');
|
||||
await cache.set('related:123', 'old-related');
|
||||
|
||||
// Simulate race: update and read concurrently
|
||||
const [updateResult, readResult] = await Promise.all([
|
||||
updateCache('user:123', 'new-value'),
|
||||
getCache('user:123'),
|
||||
]);
|
||||
|
||||
// Read should either see old (before update) or new (after update)
|
||||
// but never a mix of new + stale related
|
||||
const finalValue = await getCache('user:123');
|
||||
|
||||
expect(finalValue).not.toBe('old-related'); // Should not see stale
|
||||
expect(['old-value', 'new-value']).toContain(finalValue);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Test fails without lock, passes with lock.
|
||||
|
||||
### Implement Fix
|
||||
|
||||
Lock-based solution has performance cost. Consider alternative:
|
||||
|
||||
```typescript
|
||||
export async function updateCache(key: string, value: any): Promise<void> {
|
||||
// Generate update ID to track consistency
|
||||
const updateId = crypto.randomUUID();
|
||||
|
||||
// Set all values atomically with transaction
|
||||
await cache.multi()
|
||||
.set(key, value)
|
||||
.set(`${key}:updateId`, updateId)
|
||||
.invalidate(getRelatedKeys(key))
|
||||
.exec();
|
||||
}
|
||||
```
|
||||
|
||||
This uses cache transaction (multi/exec) for atomicity without explicit locking.
|
||||
|
||||
### Verify Fix Works
|
||||
|
||||
Run comprehensive tests:
|
||||
|
||||
```bash
|
||||
# Run 1000 times to ensure no failures
|
||||
for i in {1..1000}; do
|
||||
npm test -- --grep "cache update" > /dev/null || echo "Failed: $i"
|
||||
done
|
||||
|
||||
# Results: 0 failures
|
||||
```
|
||||
|
||||
All tests pass:
|
||||
- Race condition test passes
|
||||
- Existing tests pass
|
||||
- Load test shows no performance degradation
|
||||
- No deadlocks or timeouts
|
||||
|
||||
### Document Fix
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Updates cache value atomically with related entries.
|
||||
*
|
||||
* Bug fix (2024-12-12): Prevented race condition during updates.
|
||||
* Root cause: Async optimization (f8d3c21) introduced race window
|
||||
* between setting value and invalidating related entries. Concurrent
|
||||
* reads during this window could return inconsistent data.
|
||||
*
|
||||
* Prevention: Use cache transactions (multi/exec) to ensure atomicity.
|
||||
* All cache updates and invalidations complete together or not at all.
|
||||
*/
|
||||
export async function updateCache(key: string, value: any): Promise<void> {
|
||||
const updateId = crypto.randomUUID();
|
||||
|
||||
await cache.multi()
|
||||
.set(key, value)
|
||||
.set(`${key}:updateId`, updateId)
|
||||
.invalidate(getRelatedKeys(key))
|
||||
.exec();
|
||||
}
|
||||
```
|
||||
|
||||
### Additional Defense
|
||||
|
||||
Add monitoring to detect inconsistency:
|
||||
|
||||
```typescript
|
||||
export async function getCache(key: string): Promise<any> {
|
||||
const [value, updateId] = await Promise.all([
|
||||
cache.get(key),
|
||||
cache.get(`${key}:updateId`),
|
||||
]);
|
||||
|
||||
// Verify related entries have matching updateId
|
||||
const relatedKeys = getRelatedKeys(key);
|
||||
for (const relatedKey of relatedKeys) {
|
||||
const relatedUpdateId = await cache.get(`${relatedKey}:updateId`);
|
||||
|
||||
if (relatedUpdateId !== updateId) {
|
||||
// Log inconsistency for monitoring
|
||||
console.warn(`Cache inconsistency detected: ${key}`);
|
||||
// Trigger cache refresh
|
||||
await refreshCache(key);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
**Transition**: Fix verified and monitoring added. Mark "Verify Fix" complete.
|
||||
|
||||
**Task state**: All tasks completed.
|
||||
|
||||
## Summary
|
||||
|
||||
**What broke**: Cache occasionally returned stale data after updates
|
||||
|
||||
**Root cause**: Async optimization introduced race window between setting value and invalidating related entries. Concurrent reads during this window could read new value but get stale related data.
|
||||
|
||||
**The fix**:
|
||||
1. Use cache transactions (multi/exec) for atomic updates
|
||||
2. All updates and invalidations complete atomically
|
||||
3. Added monitoring to detect inconsistencies
|
||||
|
||||
**Prevention**:
|
||||
- Use atomic operations for multi-step cache updates
|
||||
- Test concurrent operations explicitly
|
||||
- Add timing logs to expose race conditions
|
||||
- Monitor for cache inconsistency in production
|
||||
|
||||
## Lessons
|
||||
|
||||
- Intermittent bugs require many test runs to find pattern
|
||||
- Timing logs revealed race window
|
||||
- Widening race window made bug consistent for testing
|
||||
- Async optimization can introduce race conditions
|
||||
- Atomic operations eliminate race windows
|
||||
- Transaction support in cache library enables atomicity without locks
|
||||
@@ -0,0 +1,310 @@
|
||||
# Runtime Error Debugging Session
|
||||
|
||||
This example demonstrates systematic debugging of a runtime error using the four-stage framework.
|
||||
|
||||
## The Bug
|
||||
|
||||
User reports: "Application crashes when processing certain user profiles with `TypeError: Cannot read property 'email' of undefined`"
|
||||
|
||||
## Stage 1: Collect Evidence
|
||||
|
||||
**Task state**: "Collecting evidence" (in_progress)
|
||||
|
||||
### Read Error Message
|
||||
|
||||
```
|
||||
TypeError: Cannot read property 'email' of undefined
|
||||
at formatUserDisplay (src/users/formatter.ts:42:23)
|
||||
at UserProfile (src/components/UserProfile.tsx:18:15)
|
||||
at processProfiles (src/services/profileService.ts:67:8)
|
||||
```
|
||||
|
||||
Stack trace points to line 42 in formatter.ts accessing `.email` on undefined value.
|
||||
|
||||
### Reproduce Consistently
|
||||
|
||||
Steps to reproduce:
|
||||
1. Load user profile page
|
||||
2. Navigate to profile ID: `user-incomplete-123`
|
||||
3. Error occurs consistently for this user
|
||||
4. Works fine for other users (e.g., `user-complete-456`)
|
||||
|
||||
### Check Recent Changes
|
||||
|
||||
```bash
|
||||
git log --since="2 days ago" --oneline src/users/formatter.ts
|
||||
# No recent changes to formatter.ts
|
||||
|
||||
git log --since="2 days ago" --oneline src/services/
|
||||
# 3d7a921 Optimize profile fetch to reduce API calls
|
||||
```
|
||||
|
||||
Recent commit optimized profile fetching. Potential cause.
|
||||
|
||||
### Gather Evidence
|
||||
|
||||
Added logging to formatter.ts:
|
||||
|
||||
```typescript
|
||||
export function formatUserDisplay(user: User): string {
|
||||
console.log('[DEBUG] formatUserDisplay input:', JSON.stringify(user));
|
||||
|
||||
// Line 42 - where error occurs
|
||||
const email = user.email.toLowerCase();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[DEBUG] formatUserDisplay input: {"id":"user-incomplete-123","name":"Test User"}
|
||||
TypeError: Cannot read property 'email' of undefined
|
||||
```
|
||||
|
||||
**Key finding**: User object missing `email` field entirely.
|
||||
|
||||
### Trace Data Flow Backward
|
||||
|
||||
Where does user object come from?
|
||||
|
||||
```typescript
|
||||
// profileService.ts:67
|
||||
const display = formatUserDisplay(profile.user);
|
||||
```
|
||||
|
||||
Check profile.user:
|
||||
|
||||
```typescript
|
||||
console.log('[DEBUG] profile object:', JSON.stringify(profile));
|
||||
// Output: {"id":"prof-123","user":{"id":"user-incomplete-123","name":"Test User"}}
|
||||
```
|
||||
|
||||
User object from API is missing email field.
|
||||
|
||||
**Transition**: Evidence gathered, reproduction confirmed. Mark "Collect Evidence" complete, add "Isolate Variables" as in_progress.
|
||||
|
||||
## Stage 2: Isolate Variables
|
||||
|
||||
**Task state**: "Isolating variables" (in_progress)
|
||||
|
||||
### Find Working Examples
|
||||
|
||||
Check working user profile:
|
||||
|
||||
```typescript
|
||||
// user-complete-456 returns:
|
||||
{"id":"user-complete-456","name":"Complete User","email":"user@example.com"}
|
||||
|
||||
// user-incomplete-123 returns:
|
||||
{"id":"user-incomplete-123","name":"Test User"}
|
||||
```
|
||||
|
||||
Difference: Some users don't have email field in API response.
|
||||
|
||||
### Read Reference Implementation
|
||||
|
||||
Found similar code that handles missing fields:
|
||||
|
||||
```typescript
|
||||
// src/auth/userValidator.ts
|
||||
export function validateUser(user: Partial<User>): User {
|
||||
if (!user.email) {
|
||||
throw new Error('User must have email');
|
||||
}
|
||||
return user as User;
|
||||
}
|
||||
```
|
||||
|
||||
This validates email exists before using it.
|
||||
|
||||
### Identify Differences
|
||||
|
||||
Working code:
|
||||
- Validates email exists before access
|
||||
- Handles Partial<User> type
|
||||
- Throws clear error if missing
|
||||
|
||||
Broken code:
|
||||
- Assumes email always exists
|
||||
- Direct property access
|
||||
- No validation
|
||||
|
||||
### Understand Dependencies
|
||||
|
||||
Recent optimization commit changed from:
|
||||
|
||||
```typescript
|
||||
// Old: Fetched full user details
|
||||
const user = await fetchFullUser(userId);
|
||||
```
|
||||
|
||||
To:
|
||||
|
||||
```typescript
|
||||
// New: Uses cached profile data
|
||||
const user = profile.user; // May be incomplete
|
||||
```
|
||||
|
||||
**Root cause hypothesis forming**: Optimization changed data source from full user fetch to cached profile, which may have incomplete user data.
|
||||
|
||||
**Transition**: Key differences identified. Mark "Isolate Variables" complete, add "Formulate Hypotheses" as in_progress.
|
||||
|
||||
## Stage 3: Formulate Hypotheses & Test
|
||||
|
||||
**Task state**: "Formulating hypotheses" (in_progress)
|
||||
|
||||
### Form Hypothesis
|
||||
|
||||
**Hypothesis**: "The function fails because the optimization commit (3d7a921) changed from fetching full user objects to using cached profile data, which doesn't include email for users who haven't completed onboarding. The formatter assumes email always exists, causing undefined access."
|
||||
|
||||
Evidence supporting hypothesis:
|
||||
- Error only occurs for specific users (incomplete profiles)
|
||||
- Started after optimization commit
|
||||
- Working users have email, broken users don't
|
||||
- API response shows missing email field
|
||||
|
||||
**Transition**: Hypothesis formed. Mark "Formulate Hypotheses" complete, add "Test Hypothesis" as in_progress.
|
||||
|
||||
**Task state**: "Testing hypothesis" (in_progress)
|
||||
|
||||
### Design Minimal Test
|
||||
|
||||
Temporarily revert optimization to test hypothesis:
|
||||
|
||||
```typescript
|
||||
// Change profile.user back to full fetch
|
||||
const user = await fetchFullUser(profile.userId);
|
||||
const display = formatUserDisplay(user);
|
||||
```
|
||||
|
||||
### Execute Test
|
||||
|
||||
Run with reverted code:
|
||||
- Error no longer occurs
|
||||
- All users display correctly (including user-incomplete-123)
|
||||
- Full fetch includes all required fields
|
||||
|
||||
**Result**: Hypothesis confirmed. The optimization exposed assumption that all user objects have email.
|
||||
|
||||
**Transition**: Hypothesis confirmed, ready to implement fix. Mark "Test Hypothesis" complete, add "Verify Fix" as in_progress.
|
||||
|
||||
## Stage 4: Verify Fix
|
||||
|
||||
**Task state**: "Verifying fix" (in_progress)
|
||||
|
||||
### Create Failing Test
|
||||
|
||||
```typescript
|
||||
describe('formatUserDisplay', () => {
|
||||
it('handles users without email gracefully', () => {
|
||||
const incompleteUser = {
|
||||
id: 'user-123',
|
||||
name: 'Test User',
|
||||
// email intentionally missing
|
||||
};
|
||||
|
||||
// This currently throws, should handle gracefully
|
||||
expect(() => formatUserDisplay(incompleteUser)).toThrow(
|
||||
'User email is required'
|
||||
);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Test fails as expected (throws TypeError instead of clear error).
|
||||
|
||||
### Implement Fix
|
||||
|
||||
Two options identified:
|
||||
1. Fetch full user data (removes optimization)
|
||||
2. Handle missing email in formatter (preserves optimization)
|
||||
|
||||
Choose option 2 to preserve optimization and add defensive validation:
|
||||
|
||||
```typescript
|
||||
export function formatUserDisplay(user: User): string {
|
||||
// Input validation
|
||||
if (!user.email || typeof user.email !== 'string') {
|
||||
throw new Error(`User email is required for display. User ID: ${user.id}`);
|
||||
}
|
||||
|
||||
const email = user.email.toLowerCase();
|
||||
const name = user.name || 'Unknown';
|
||||
|
||||
return `${name} <${email}>`;
|
||||
}
|
||||
```
|
||||
|
||||
### Verify Fix Works
|
||||
|
||||
Run tests:
|
||||
- Failing test now passes (throws clear error message)
|
||||
- All existing tests pass
|
||||
- Manual reproduction: Clear error message instead of crash
|
||||
- Profile service can catch error and handle incomplete users
|
||||
|
||||
### Document Fix
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Formats user information for display.
|
||||
*
|
||||
* Bug fix (2024-12-12): Added email validation.
|
||||
* Root cause: Profile optimization (3d7a921) changed data source
|
||||
* from full user fetch to cached profile data. Some users haven't
|
||||
* completed onboarding and lack email field.
|
||||
* Prevention: Always validate required fields before access.
|
||||
*
|
||||
* @throws {Error} If user.email is missing or invalid
|
||||
*/
|
||||
export function formatUserDisplay(user: User): string {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Additional Defense
|
||||
|
||||
Update profile service to handle incomplete users:
|
||||
|
||||
```typescript
|
||||
// src/services/profileService.ts
|
||||
try {
|
||||
const display = formatUserDisplay(profile.user);
|
||||
return { ...profile, display };
|
||||
} catch (error) {
|
||||
// User hasn't completed onboarding, fetch full details
|
||||
const fullUser = await fetchFullUser(profile.userId);
|
||||
const display = formatUserDisplay(fullUser);
|
||||
return { ...profile, display };
|
||||
}
|
||||
```
|
||||
|
||||
**Transition**: Fix verified and deployed. Mark "Verify Fix" complete.
|
||||
|
||||
**Task state**: All tasks completed.
|
||||
|
||||
## Summary
|
||||
|
||||
**What broke**: TypeError when accessing email property on undefined
|
||||
|
||||
**Root cause**: Optimization changed data source from full user fetch to cached profile data. Cached data incomplete for users who haven't finished onboarding.
|
||||
|
||||
**The fix**:
|
||||
1. Added input validation in formatter
|
||||
2. Profile service falls back to full fetch for incomplete users
|
||||
3. Clear error messages guide developers
|
||||
|
||||
**Prevention**:
|
||||
- Always validate required fields before access
|
||||
- Consider data completeness when optimizing data fetching
|
||||
- Add tests for incomplete/partial data scenarios
|
||||
|
||||
## Lessons
|
||||
|
||||
- Error message stack trace led directly to problem location
|
||||
- Recent changes (git log) identified likely cause
|
||||
- Comparing working vs broken cases revealed pattern
|
||||
- Single hypothesis tested with minimal change
|
||||
- Fix addresses root cause while preserving optimization
|
||||
- Defensive validation at multiple layers prevents recurrence
|
||||
Reference in New Issue
Block a user