Silent bugs are the hardest: When code runs but gives wrong results
browser() freezes time: You can inspect the environment mid-execution
Step through code: Execute line by line and watch variables change
Print at will: Check what any variable contains at that exact moment
Browser Commands Reference
Command
What it does
n
Execute next line
f
Finish execution next line
s
Step into function
c
Continue execution
where
Print current location
ls()
List variables in environment
print(variable_name)
Check a variable’s value
Q
Quit debugger
Live Demo: Browsing the Code
calculate_final_price <-function(base_price, discount) {browser() # freeze! discount_amount <- base_price * (discount /100) final <- base_price + discount_amount # BUGreturn(final)}calculate_final_price(200, 15)# Now you can type commands:# > base_price# > discount # > n (to step to next line)
Set Breakpoints
Essentially acts as browser() but without modifying your code.
Message Only: Just shows the error message, no stack info
Error Inspector: Gives you button to see traceback, but you have to click it
Break in Code: Enters debug mode immediately on error, allow you to inspect variables
Pro tip: Use “Break in Code” to get hands-on experience with debugging.
On VSCode and Positron
VSCode R extension supports debugging
Positron offers similar
A bit tricky to set up …
You get a full fledged debugging experience with breakpoints, variable inspection, and more..
Catching Errors Early: debug() and debugonce()
debug() - Persistent Debugging
# Set debug mode for a functiondebug(run_analysis)# enter debug moderun_analysis(c(1, 2, 3))# Later turn it offundebug(run_analysis)
Use when:
You want to inspect a function’s internals
Need to trace variables through multiple calls
debugonce() - One-Time Debugging
# Debug the next call onlydebugonce(clean_data)# First call enters debug modeclean_data(c(1, 2, 3))# Second call runs normallyclean_data(c(1, 2, 3))
Use when:
You only want to inspect one specific call
Testing different arguments without full debug
Advanced: Conditional Debugging
# Example: Debug only when a condition is truefor (i in1:1000) { result <-process_data(data[i])# Enter debug mode only for problematic rowsif (is.na(result)) {browser() # Freezes only when result is NA }}# Or with debug() for specific function calls:if (my_condition) {debugonce(problematic_function)}
Benefits:
Avoid stepping through 1000 iterations manually
Target only the rows/cases that fails!
Combine browser() with conditions for precision
Case Study: The Black Box
The Setup Scene
my_model <-lm(salary ~ education + experience + city, data = survey_data)# > Error in `contrasts<-`(`*tmp*`, value = contr.funs[1 + isOF[nn]]) : # > contrasts can be applied only to factors with 2 or more levels
Step 1: The traceback()
Running traceback() shows us the internal guts of the lm() function:
traceback()# > 5: stop("contrasts can be applied only to factors with 2 or more levels")# > 4: `contrasts<-`(`*tmp*`, value = contr.funs[1 + isOF[nn]])# > 3: model.matrix.default(mt, mf, contrasts)# > 2: model.matrix(mt, mf, contrasts)# > 1: lm(salary ~ education + experience + city, data = survey_data)
What we know:
lm() called model.matrix(), which crashed on step 4.
What we don’t know:
Which column in our data caused model.matrix to choke.
Step 2: options(error = recover)
When you can’t put browser() in the code, you change R’s global rules.
Same thing as break in code in RStudio debug mode
options(error = recover)my_model <-lm(salary ~ education + experience + city, data = survey_data)
Step 3: Entering the Matrix
R immediately pauses and gives you a menu of the call stack.
# Error in `contrasts<-`(`*tmp*`, value = contr.funs[1 + isOF[nn]]) : # contrasts can be applied only to factors with 2 or more levels# Enter a frame number, or 0 to exit # 1: lm(salary ~ education + experience + city, data = survey_data)# 2: model.matrix(mt, mf, contrasts)# 3: model.matrix.default(mt, mf, contrasts)# 4: `contrasts<-`(`*tmp*`, value = contr.funs[1 + isOF[nn]])# Selection: 3
Step 4: The Interrogation
You are now inside the package’s environment. You can check the variables the package author was using.
Now, we copy the output from dput() and combine it with our broken code. We highlight it, copy it to our clipboard, and type reprex::reprex() in the console.
# Data generated perfectly by dput()tiny_diamonds <-structure(list(carat =c(0.23, 0.21, 0.23), cut =ordered(c(5L, 4L, 2L), levels =c("Fair", "Good", "Very Good", "Premium", "Ideal")), color =ordered(c(2L, 2L, 2L), levels =c("D", "E", "F", "G", "H", "I", "J")), clarity =ordered(c(2L, 3L, 5L), levels =c("I1", "SI2", "SI1", "VS2", "VS1", "VVS2", "VVS1", "IF")), depth =c(61.5, 59.8, 56.9), table =c(55, 61, 65), price =c(326L, 326L, 327L), x =c(3.95, 3.89, 4.05), y =c(3.98, 3.84, 4.07), z =c(2.43, 2.31, 2.31)), row.names =c(NA, -3L), class =c("tbl_df", "tbl", "data.frame"))cor(tiny_diamonds)#first copy this above and the run reprex::reprex() and then sessionInfo()
Note: Also never forget to give you session info in your reprex, so that people know which version of R and packages you are using. You can do this with sessionInfo().
Note on Anonymization
We can use FakeDataR to generate synthetic data that mimics the structure of our original dataset.
library(FakeDataR)# generate fake data maintaining exact column structures and factorssafe_diamonds <-generate_fake_data(data = diamonds,n =3,seed =123,category_mode ="generic",numeric_mode ="range",column_mode ="keep",sensitive_detect =TRUE,normalize =TRUE)dput(safe_diamonds)
The Modern Debugging Workflow
Encounter problem
Use your debugging tools
Create reprex (with data)
Share to LLM or GitHub
You’re not just helping others, you’re helping yourself.
When I had to create an issue on the ggplot2repository
Your Complete Debugging Arsenal
The Complete Dev’s Toolkit
debug() / debugonce() – Step into functions from the start