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:
- Flipper β Network inspector, Hermes profiler, React DevTools, Layout Inspector
- Sentry β Error tracking with source maps, breadcrumbs, session replay
- Crashlytics β Native crash reporting (separate from JS errors)
- Reactotron β State inspection, API monitoring, custom logging
- Android Studio Profiler / Xcode Instruments β Native-level profiling
- React DevTools Profiler β Component render analysis
π» Code Example
1// === REAL-WORLD DEBUGGING SCENARIOS ===23// SCENARIO 1: Memory leak investigation4// Symptom: App memory grows by 50MB over 30 minutes of normal use56// Step 1: Instrument memory tracking7class MemoryMonitor {8 private readings: { timestamp: number; jsHeap: number; nativeHeap: number }[] = [];910 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 module17 });1819 // Alert if growth rate is abnormal20 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 seconds24 ErrorReporter.report(new Error(`Memory leak detected: ${growth} bytes growth`), {25 readings: this.readings.slice(-10),26 });27 }28 }29 }, intervalMs);30 }31}3233// Step 2: Screen-level leak detection34function useScreenMemoryCheck(screenName: string) {35 useEffect(() => {36 const mountJsHeap = performance?.memory?.usedJSHeapSize ?? 0;3738 return () => {39 // Schedule check for AFTER GC has a chance to run40 setTimeout(() => {41 const unmountJsHeap = performance?.memory?.usedJSHeapSize ?? 0;42 const retained = unmountJsHeap - mountJsHeap;4344 if (retained > 5 * 1024 * 1024) { // 5MB retained after unmount45 analytics.track('potential_memory_leak', {46 screen: screenName,47 retainedBytes: retained,48 });49 }50 }, 3000); // Wait for GC51 };52 }, []);53}5455// SCENARIO 2: Race condition debugging56// Symptom: Sometimes the profile screen shows the wrong user's data5758// The bug:59function ProfileScreen({ userId }: { userId: string }) {60 const [profile, setProfile] = useState<User | null>(null);6162 useEffect(() => {63 // β RACE CONDITION: If userId changes quickly,64 // the response from the first request might arrive AFTER the second65 api.getUser(userId).then(setProfile);66 }, [userId]);6768 // User A's profile loads β userId changes to B β B's request starts69 // β A's response arrives (slow network) β shows A's data for B's screen!70}7172// The fix:73function ProfileScreen({ userId }: { userId: string }) {74 const [profile, setProfile] = useState<User | null>(null);7576 useEffect(() => {77 let cancelled = false;78 const controller = new AbortController();7980 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 });8990 return () => {91 cancelled = true;92 controller.abort(); // Actually cancel the network request93 };94 }, [userId]);95}9697// SCENARIO 3: Bridge overload (old architecture)98// Symptom: App becomes unresponsive during scroll99100// Diagnostic: MessageQueue spy101import MessageQueue from 'react-native/Libraries/BatchedBridge/MessageQueue';102103if (__DEV__) {104 MessageQueue.spy((msg) => {105 // Log bridge messages to find excessive traffic106 if (msg.type === 0) { // JS β Native call107 console.log(`Bridge: JSβNative ${msg.module}.${msg.method}`);108 }109 });110}111112// 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 useNativeDriver116// 4. Frequent state updates triggering multiple bridge round-trips117118// SCENARIO 4: Crash analytics investigation119// Production crash: "TypeError: Cannot read property 'name' of undefined"120121// Step 1: Check Sentry breadcrumbs122// What happened before the crash?123// Navigation: Home β Feed β Profile β CRASH124// API call: GET /user/123 β 200 OK125// State update: setUser(response.data)126127// Step 2: The response.data was { user: null } (user deleted)128// Step 3: Code assumed user always exists129130// Prevention: Defensive coding + runtime validation131function ProfileScreen({ route }) {132 const { data, error, isLoading } = useUser(route.params.userId);133134 // Guard every access β never assume shape135 if (isLoading) return <Skeleton />;136 if (error) return <ErrorState error={error} />;137 if (!data) return <NotFoundState message="User not found" />;138139 // Now TypeScript narrows data to User (non-null)140 return <ProfileView user={data} />;141}
ποΈ Practice Exercise
Debugging Exercises:
- Deliberately create a memory leak (event listener in useEffect without cleanup) and use Hermes profiler to find it
- Create a race condition in a search component (fast typing + slow API) and fix it with AbortController
- Set up Sentry in your app with source maps and trigger an error to see the full stack trace
- Use Flipper to inspect network requests and identify a slow API call causing UI lag
- Profile your app's re-renders using React DevTools Profiler β find the costliest component
- 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.