As React Native developers, we're comfortable with JavaScript debugging—Chrome DevTools, console logs, the usual suspects. But native code? That's where things get intimidating.
Here's what I learned about debugging Kotlin/Swift code in React Native apps, and why it's not as scary as you think.
Why Bother with Native Debugging?
- Third-party library crashes: That cryptic native error? Step through the actual code to see what's breaking.
- Custom native modules: Debug your bridge code properly instead of littering it with log statements.
- Performance issues: JavaScript profiling won't show native rendering bottlenecks.
- Platform-specific bugs: That Android 12-only crash? See exactly where and why it fails.
The Debugging Tools
Android Studio (Kotlin/Java)
Android Studio is your IDE for the Android native side. It's built on IntelliJ, so if you've used WebStorm or other JetBrains tools, the interface will feel familiar.
Key debugging shortcuts:
-
F8- Step Over (execute current line, move to next) -
F7- Step Into (dive into function call) -
Shift+F8- Step Out (finish current function, return to caller) -
F9- Resume (continue until next breakpoint) -
Cmd+F8- Toggle breakpoint
Xcode (Swift/Objective-C)
Xcode is Apple's IDE for iOS development. It has powerful debugging tools, though the keyboard shortcuts differ from Android Studio.
Key debugging shortcuts:
-
F6- Step Over -
F7- Step Into -
F8- Step Out - Control button (or
Ctrl+Cmd+Y) - Continue/Resume -
Cmd+\- Toggle breakpoint
Setting Up: My Practice Code
To really learn debugging, I added sample code to both platforms with common patterns you'll encounter. Here's what I created:
Android (MainActivity.kt)
private fun debugBreakpointDemo() {
val userName = "ReactNativeDev"
val userAge = 28
val isActive = true
val items = listOf("Apple", "Banana", "Cherry", "Date", "Elderberry")
for ((index, item) in items.withIndex()) {
Log.d("BreakpointDemo", "Item $index: $item")
if (index == 2) {
val specialItem = item.uppercase()
Log.d("BreakpointDemo", "Special item found: $specialItem")
}
}
val result = performCalculation(10, 20)
Log.d("BreakpointDemo", "Calculation result: $result")
}
private fun performCalculation(a: Int, b: Int): Int {
val sum = a + b
val multiplied = sum * 2
return multiplied
}
iOS (AppDelegate.swift)
func debugBreakpointDemo() {
let userName = "ReactNativeDev"
let userAge = 28
let isActive = true
let items = ["Apple", "Banana", "Cherry", "Date", "Elderberry"]
for (index, item) in items.enumerated() {
print("Item \(index): \(item)")
if index == 2 {
let specialItem = item.uppercased()
print("Special item found: \(specialItem)")
}
}
let result = performCalculation(a: 10, b: 20)
print("Calculation result: \(result)")
}
func performCalculation(a: Int, b: Int) -> Int {
let sum = a + b
let multiplied = sum * 2
return multiplied
}
Hands-On: Your First Native Debugging Session
Let me walk you through a practice session. This is how I learned, and it demystified native debugging completely.
Step 1: Set Your First Breakpoint
Android Studio:
- Open
android/app/src/main/java/.../MainActivity.kt - Find the line
val userName = "ReactNativeDev" - Click the gray gutter area to the left of the line number
- A red dot appears—that's your breakpoint
Xcode:
- Open
ios/YourApp/AppDelegate.swift - Find the line
let userName = "ReactNativeDev" - Click the gray gutter to the left
- A blue arrow appears—that's your breakpoint
Step 2: Run in Debug Mode
Android Studio:
- Click the bug icon 🪲 in the toolbar (or press
Shift+F9) - The app launches and immediately pauses at your breakpoint
- The IDE highlights the current line in blue
Xcode:
- Press
Cmd+Ror click the Play button - The app pauses at your breakpoint
- The current line is highlighted with a green overlay
Step 3: Explore the Variables Panel
This is where the magic happens. Look at the bottom of your IDE.
You'll see a "Variables" panel showing:
userName = "ReactNativeDev"
userAge = <not initialized yet>
isActive = <not initialized yet>
Why aren't the others initialized? Because breakpoints pause before executing the line. The line is about to run but hasn't yet.
Step 4: Step Over (F8 or F6)
Press F8 (Android Studio) or F6 (Xcode) a few times. Watch as:
-
userNameappears in variables - Next step:
userAgeappears - Next step:
isActiveappears - The list
itemsgets created
You're executing line by line. This is stepping over—you execute the current line and move to the next one without diving into function internals.
Step 5: Step Into a Function (F7)
Keep stepping until you reach the line:
val result = performCalculation(10, 20)
Now instead of pressing F8/F6, press F7.
Boom! You're now inside the performCalculation function. The IDE jumped you to its definition. This is stepping into—diving into the implementation of a function call.
Step over a couple of times inside this function. Watch sum and multiplied appear in the variables panel.
Step 6: Step Out (Shift+F8 or F8)
You've seen enough inside performCalculation. Press Shift+F8 (Android Studio) or F8 (Xcode).
The function completes, and you're back where you called it from. The result variable now contains the return value (60). This is stepping out—finishing the current function and returning to the caller.
Step 7: Resume Execution (F9 or Continue Button)
Press F9 (Android Studio) or click the Continue button (Xcode) to let the app run normally until it hits another breakpoint (or finishes).
Advanced Techniques I Use Daily
Conditional Breakpoints
Right-click a breakpoint and add a condition. For example, only break when index == 2 in a loop. This saved me hours when debugging issues that only occurred on the 100th iteration.
Android Studio: Right-click breakpoint → More → Condition
Xcode: Right-click breakpoint → Edit Breakpoint → Condition
Expression Evaluation
While paused, you can evaluate any expression on the fly.
Android Studio:
- Select an expression in code
- Right-click → Evaluate Expression (or
Alt+F8) - Try:
items.filter { it.startsWith("A") }
Xcode:
- In the debug console, type:
po items.filter { $0.hasPrefix("A") } -
pomeans "print object"
Call Stack Navigation
The call stack shows how you got to the current line. Click any frame to jump to that context and see its variables. Invaluable when you're deep in a chain of function calls.
Watch Variables
Add variables to your watch list to track them across function calls. They persist even when out of scope.
Android Studio: Right-click variable → Add to Watches
Xcode: Right-click variable → Watch "variableName"
Real-World Example: Debugging a Crash
Last week, my app crashed when users tapped a button. JavaScript logs showed nothing. Here's how native debugging saved me:
-
Android Studio: Set breakpoint in
onClickhandler - Run in debug mode, tap the button
- Step through the native code
- Found the crash: accessing an ArrayList index that didn't exist
- The issue: JavaScript sent wrong array length to native
Without stepping through with breakpoints, I'd have spent hours adding log statements and rebuilding.
Tips from the Trenches
Start simple: Don't try to debug react-native core libraries on day one. Practice with your own simple native code first.
Use both IDEs: I have both Android Studio and Xcode open when working on RN. Debug both platforms regularly—they have different bugs.
Combine with JS debugging: Use both Chrome DevTools for JS and native debuggers. Some bugs span both layers.
Read the call stack: When you hit a breakpoint, spend time reading the call stack. Understanding how you got there is as important as where you are.
Don't fear native code: It's just code. Kotlin and Swift are actually quite pleasant languages. The debugging process is identical to JavaScript debugging, just different tools.
Practice Challenge
Here's what I recommend: spend 30 minutes today just stepping through the sample code I showed above. Don't try to fix anything, just explore:
- Set breakpoints in loops
- Step into and out of functions
- Watch variables change
- Evaluate expressions
- Add conditional breakpoints
This low-pressure practice builds the muscle memory. When you hit a real bug, you'll know exactly what to do.
Resources That Helped Me
- Android Developer Guide - Debug Your App
- Apple Developer - Debugging
- React Native - Native Debugging
Conclusion
Native debugging isn't scary—it's empowering. As React Native developers, we shouldn't fear diving into the native layer. These tools are powerful, learnable, and absolutely essential for professional RN development.
The difference between a good RN developer and a great one? Great ones are comfortable in all three worlds: JavaScript, Android native, and iOS native.
Start small, practice deliberately, and soon you'll wonder how you ever developed without native debugging.
Happy debugging! 🐛🔍
What native debugging techniques have you found helpful? Share your experiences in the comments below!



Top comments (0)