Systematic Debugging Mindset

πŸ“– Concept

Production debugging at the senior level is systematic, not ad-hoc. You need a mental framework that works for ANY problem, not memorized solutions for specific bugs.

The CLEAR debugging framework:

C β€” Collect evidence (logs, crash reports, user reports, metrics)
L β€” Localize the problem (which layer? JS? Native? Network? State?)
E β€” Establish hypotheses (ranked by likelihood)
A β€” Act on highest-probability hypothesis (instrument, reproduce, verify)
R β€” Remediate and prevent (fix, monitor, post-mortem)

Layer isolation for React Native:

Problem Symptoms β†’ Localization Questions

UI not updating β†’ Is state changing? (React DevTools)
                β†’ Is reconciliation running? (Profiler)
                β†’ Is native view updating? (Layout Inspector)

App crashes     β†’ JS exception? (error boundary, Sentry)
                β†’ Native crash?  (Crashlytics, Android logcat, iOS crash log)
                β†’ OOM? (memory profiler)

Slow performance β†’ JS thread blocked? (Hermes profiler)
                 β†’ Bridge congestion? (MessageQueue spy)
                 β†’ Native slow? (Android Systrace, iOS Instruments)
                 β†’ Too many re-renders? (React Profiler)

Network issues  β†’ Request sent? (Flipper network inspector)
                β†’ Response received? (status code, timing)
                β†’ Data parsed correctly? (response shape)
                β†’ State updated? (state inspection)

Tools for RN debugging at scale:

  1. Flipper β€” Network inspector, Hermes profiler, React DevTools, Layout Inspector
  2. Sentry β€” Error tracking with source maps, breadcrumbs, session replay
  3. Crashlytics β€” Native crash reporting (separate from JS errors)
  4. Reactotron β€” State inspection, API monitoring, custom logging
  5. Android Studio Profiler / Xcode Instruments β€” Native-level profiling
  6. React DevTools Profiler β€” Component render analysis

πŸ’» Code Example

codeTap to expand β›Ά
1// === REAL-WORLD DEBUGGING SCENARIOS ===
2
3// SCENARIO 1: Memory leak investigation
4// Symptom: App memory grows by 50MB over 30 minutes of normal use
5
6// Step 1: Instrument memory tracking
7class MemoryMonitor {
8 private readings: { timestamp: number; jsHeap: number; nativeHeap: number }[] = [];
9
10 start(intervalMs = 5000) {
11 setInterval(() => {
12 const jsHeap = performance?.memory?.usedJSHeapSize ?? 0;
13 this.readings.push({
14 timestamp: Date.now(),
15 jsHeap,
16 nativeHeap: 0, // Requires native module
17 });
18
19 // Alert if growth rate is abnormal
20 if (this.readings.length >= 10) {
21 const oldestRecent = this.readings[this.readings.length - 10];
22 const growth = jsHeap - oldestRecent.jsHeap;
23 if (growth > 10 * 1024 * 1024) { // 10MB growth in 50 seconds
24 ErrorReporter.report(new Error(`Memory leak detected: ${growth} bytes growth`), {
25 readings: this.readings.slice(-10),
26 });
27 }
28 }
29 }, intervalMs);
30 }
31}
32
33// Step 2: Screen-level leak detection
34function useScreenMemoryCheck(screenName: string) {
35 useEffect(() => {
36 const mountJsHeap = performance?.memory?.usedJSHeapSize ?? 0;
37
38 return () => {
39 // Schedule check for AFTER GC has a chance to run
40 setTimeout(() => {
41 const unmountJsHeap = performance?.memory?.usedJSHeapSize ?? 0;
42 const retained = unmountJsHeap - mountJsHeap;
43
44 if (retained > 5 * 1024 * 1024) { // 5MB retained after unmount
45 analytics.track('potential_memory_leak', {
46 screen: screenName,
47 retainedBytes: retained,
48 });
49 }
50 }, 3000); // Wait for GC
51 };
52 }, []);
53}
54
55// SCENARIO 2: Race condition debugging
56// Symptom: Sometimes the profile screen shows the wrong user's data
57
58// The bug:
59function ProfileScreen({ userId }: { userId: string }) {
60 const [profile, setProfile] = useState<User | null>(null);
61
62 useEffect(() => {
63 // ❌ RACE CONDITION: If userId changes quickly,
64 // the response from the first request might arrive AFTER the second
65 api.getUser(userId).then(setProfile);
66 }, [userId]);
67
68 // User A's profile loads β†’ userId changes to B β†’ B's request starts
69 // β†’ A's response arrives (slow network) β†’ shows A's data for B's screen!
70}
71
72// The fix:
73function ProfileScreen({ userId }: { userId: string }) {
74 const [profile, setProfile] = useState<User | null>(null);
75
76 useEffect(() => {
77 let cancelled = false;
78 const controller = new AbortController();
79
80 api.getUser(userId, { signal: controller.signal })
81 .then(data => {
82 if (!cancelled) setProfile(data);
83 })
84 .catch(err => {
85 if (err.name !== 'AbortError' && !cancelled) {
86 ErrorReporter.report(err);
87 }
88 });
89
90 return () => {
91 cancelled = true;
92 controller.abort(); // Actually cancel the network request
93 };
94 }, [userId]);
95}
96
97// SCENARIO 3: Bridge overload (old architecture)
98// Symptom: App becomes unresponsive during scroll
99
100// Diagnostic: MessageQueue spy
101import MessageQueue from 'react-native/Libraries/BatchedBridge/MessageQueue';
102
103if (__DEV__) {
104 MessageQueue.spy((msg) => {
105 // Log bridge messages to find excessive traffic
106 if (msg.type === 0) { // JS β†’ Native call
107 console.log(`Bridge: JS→Native ${msg.module}.${msg.method}`);
108 }
109 });
110}
111
112// Common causes of bridge overload:
113// 1. Passing large objects through bridge (images, JSON payloads)
114// 2. Calling native methods in scroll handlers (onScroll β†’ bridge call per frame)
115// 3. Animated values not using useNativeDriver
116// 4. Frequent state updates triggering multiple bridge round-trips
117
118// SCENARIO 4: Crash analytics investigation
119// Production crash: "TypeError: Cannot read property 'name' of undefined"
120
121// Step 1: Check Sentry breadcrumbs
122// What happened before the crash?
123// Navigation: Home β†’ Feed β†’ Profile β†’ CRASH
124// API call: GET /user/123 β†’ 200 OK
125// State update: setUser(response.data)
126
127// Step 2: The response.data was { user: null } (user deleted)
128// Step 3: Code assumed user always exists
129
130// Prevention: Defensive coding + runtime validation
131function ProfileScreen({ route }) {
132 const { data, error, isLoading } = useUser(route.params.userId);
133
134 // Guard every access β€” never assume shape
135 if (isLoading) return <Skeleton />;
136 if (error) return <ErrorState error={error} />;
137 if (!data) return <NotFoundState message="User not found" />;
138
139 // Now TypeScript narrows data to User (non-null)
140 return <ProfileView user={data} />;
141}

πŸ‹οΈ Practice Exercise

Debugging Exercises:

  1. Deliberately create a memory leak (event listener in useEffect without cleanup) and use Hermes profiler to find it
  2. Create a race condition in a search component (fast typing + slow API) and fix it with AbortController
  3. Set up Sentry in your app with source maps and trigger an error to see the full stack trace
  4. Use Flipper to inspect network requests and identify a slow API call causing UI lag
  5. Profile your app's re-renders using React DevTools Profiler β€” find the costliest component
  6. Create a crash reporting pipeline that captures: error, breadcrumbs, device info, and app state

⚠️ Common Mistakes

  • Debugging by guessing instead of systematic investigation β€” always collect evidence first, then form hypotheses

  • Not setting up error tracking (Sentry) before launch β€” you can't debug what you can't see; crashes in production are invisible without tracking

  • Ignoring non-fatal errors β€” warning-level errors often indicate bugs that will become critical under different conditions

  • Not correlating crashes with app version/device/OS β€” a crash might only affect Android 11 on Samsung devices due to a vendor-specific behavior

  • Removing debug logging before understanding the issue β€” keep diagnostic code until the fix is verified in production

πŸ’Ό Interview Questions

🎀 Mock Interview

Mock interview is powered by AI for Systematic Debugging Mindset. Login to unlock this feature.