Mobile App Debugging Rubric
A structured approach to diagnosing and fixing mobile app issues. Use this rubric during the /mobileappfix workflow.
Diagnosis Flow
┌─────────────────────────────────────────────────────────────────────┐
│ SYMPTOM IDENTIFICATION │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. CATEGORIZE THE SYMPTOM │
│ ├─► App crash/freeze │
│ ├─► UI not rendering │
│ ├─► Navigation broken │
│ ├─► Data not loading │
│ ├─► Maestro test failing │
│ └─► Build/compile error │
│ │
│ 2. COLLECT EVIDENCE │
│ ├─► Maestro screenshots │
│ ├─► Metro bundler logs │
│ ├─► Native crash logs (Console.app / logcat) │
│ ├─► React Query devtools (if available) │
│ └─► Network requests (Flipper / React Native Debugger) │
│ │
│ 3. ISOLATE THE FAILURE │
│ ├─► Which screen? │
│ ├─► Which user action? │
│ ├─► Which data state? │
│ └─► Reproducible? │
│ │
└─────────────────────────────────────────────────────────────────────┘
Category 1: App Crash/Freeze
Symptoms
- App closes unexpectedly
- UI freezes, unresponsive
- Blank white/black screen
Diagnosis Steps
Check Metro bundler
# Look for red error messages # Common: undefined is not an object # Common: Cannot read property 'x' of undefinedCheck native crash logs
# iOS open -a Console # Filter: [your-app-name], crash # Android adb logcat -s AndroidRuntime:ECheck for memory issues
# iOS: Look for "Memory pressure" in Console # Android: Look for "OutOfMemoryError" in logcat
Common Causes & Fixes
| Cause | Evidence | Fix |
|---|---|---|
| Null reference | "undefined is not an object" | Add null checks, optional chaining |
| Infinite loop | UI freeze, high CPU | Check useEffect dependencies |
| Memory leak | Slow degradation | Clean up subscriptions, timers |
| Native module crash | Crash without JS error | Check native logs, rebuild |
Category 2: UI Not Rendering
Symptoms
- Blank screen
- Loading spinner never stops
- Elements missing
- Wrong layout
Diagnosis Steps
Check component tree
// Add console.log in render console.log("MyComponent rendering", { props, state });Check conditional rendering
// Common issue: early return before data loads if (isLoading) return <Spinner />; if (!data) return null; // Never reaches actual contentCheck style issues
// Common: height: 0, opacity: 0, position off-screen // Use: { borderWidth: 1, borderColor: 'red' } to debug
Common Causes & Fixes
| Cause | Evidence | Fix |
|---|---|---|
| Query not fetching | isLoading forever | Check query enabled condition |
| Wrong conditional | Component returns early | Verify if/else logic |
| Style issues | Element exists but invisible | Debug with borders |
| Key issues | Wrong elements render | Use unique keys in lists |
Category 3: Navigation Broken
Symptoms
- Redirect loops
- Wrong screen displayed
- Back button not working
- Deep link fails
Diagnosis Steps
Check navigation guards
// util/guards/RequireAuth.tsx // Log auth state and redirects console.log("Guard check:", { isAuthenticated, programStatus });Check expo-router state
import { usePathname, useSegments } from 'expo-router'; console.log("Current path:", usePathname()); console.log("Segments:", useSegments());Check auth decision tree
app/index.tsx: isLoading? → Spinner !isAuthenticated? → /(auth) programStatus === "NO_PROGRAM"? → /(onboarding) else → /(app)
Common Causes & Fixes
| Cause | Evidence | Fix |
|---|---|---|
| Guard timing | Flicker, wrong redirect | Add loading states |
| Stale auth state | Logged in but redirected | Check session query |
| Wrong route | Ends up in unexpected screen | Verify routing logic |
| Missing layout | Screen crashes | Check _layout.tsx files |
Category 4: Data Not Loading
Symptoms
- Empty lists
- Stale data
- "No data" message
- Infinite loading
Diagnosis Steps
Check React Query state
const { data, isLoading, error, isError } = useQuery(...); console.log("Query state:", { data, isLoading, error });Check network requests
- Use Flipper or React Native Debugger
- Check Network tab for requests
- Verify response status and body
Check Supabase queries
const { data, error } = await supabase .from('table') .select('*'); console.log("Supabase result:", { data, error });
Common Causes & Fixes
| Cause | Evidence | Fix |
|---|---|---|
| Query disabled | No network request | Check enabled option |
| Auth required | 401 error | Ensure session is passed |
| Wrong query key | Stale/wrong data | Verify query key factory |
| RLS policy | Empty array, no error | Check Supabase RLS |
| Cache not invalidating | Stale after mutation | Call invalidateQueries |
Category 5: Maestro Test Failing
Symptoms
- Element not found
- Assertion failed
- Timeout
- App crash during test
Diagnosis Steps
Check element detection
maestro hierarchy # Look for resource-id matching your testIDCheck screenshots
ls .maestro/screenshots/ # Find the last screenshot before failureCheck accessibility
// TouchableWithoutFeedback breaks testID detection on iOS <TouchableWithoutFeedback accessible={false}>
Common Causes & Fixes
| Cause | Evidence | Fix |
|---|---|---|
| Missing testID | Element not in hierarchy | Add testID prop |
| Accessibility wrapper | All text merged | Add accessible={false} |
| Timing | Element not loaded yet | Increase timeout, add wait |
| Wrong selector | Finds nothing | Use maestro hierarchy to debug |
| Expo dev client | clearState fails | Use clearState: false |
Category 6: Build/Compile Error
Symptoms
- npm run ios fails
- Metro bundle error
- TypeScript error
- Pod install fails
Diagnosis Steps
Clean and rebuild
# JavaScript rm -rf node_modules .expo npm install npm start --reset-cache # iOS cd ios && rm -rf Pods Podfile.lock pod install && cd .. npm run ios # Android cd android && ./gradlew clean && cd .. npm run androidCheck TypeScript
npx tsc --noEmit # Fix all type errorsCheck ESLint
npm run lint # Fix all lint errors
Common Causes & Fixes
| Cause | Evidence | Fix |
|---|---|---|
| Dependency mismatch | Version conflict | Delete node_modules, reinstall |
| Native cache | Old binaries | Clean build, rebuild native |
| Missing pod | iOS build fails | pod install |
| Type error | tsc fails | Fix TypeScript errors |
| Babel config | Metro fails | Check babel.config.js |
Debugging Tools
Metro Bundler
# Start with fresh cache
npm start --reset-cache
# Enable verbose logging
npm start -- --verbose
React Native Debugger
# Install
brew install --cask react-native-debugger
# Open and connect
open "rndebugger://set-debugger-loc?host=localhost&port=8081"
Flipper
# Install
brew install --cask flipper
# Useful plugins:
# - Network
# - React DevTools
# - Databases (AsyncStorage)
iOS Console
# Open Console.app
open -a Console
# Filter by process: [your-app-name]
# Filter by type: error, fault
Android Logcat
# All React Native logs
adb logcat -s ReactNative:V ReactNativeJS:V
# Crashes only
adb logcat -s AndroidRuntime:E
# Clear and follow
adb logcat -c && adb logcat
Fix Verification Checklist
Before marking a fix complete:
- Root cause identified and documented
- Fix applied with minimal changes
- Linters pass:
npm run lint && npm run typecheck - Unit tests pass:
npm run test:run - Maestro smoke test passes:
maestro test .maestro/journeys/J2-*.yaml - Screenshots captured as evidence
- Changes committed with descriptive message
Anti-Patterns to Avoid
Symptom masking
- DON'T: Add try/catch that swallows errors
- DO: Fix the underlying cause
Excessive null checks
- DON'T: Add
?.everywhere - DO: Ensure data is loaded before accessing
- DON'T: Add
Force updates
- DON'T: Add
key={Math.random()}to force re-render - DO: Fix the state/prop flow
- DON'T: Add
Timeout band-aids
- DON'T: Add
setTimeoutto "fix" timing issues - DO: Use proper async/await, loading states
- DON'T: Add
Ignoring warnings
- DON'T: Suppress warnings without understanding
- DO: Fix the root cause of warnings