Debugging R Code: browser(), debug(), traceback() & RStudio Debugger
Debugging is finding out why your code produces wrong results or errors. R provides traceback() to see where an error occurred, browser() to pause and inspect, and debug() to step through functions line by line. Combined with RStudio's visual debugger, you have everything you need.
Everyone writes buggy code. The difference between a beginner and an experienced R programmer isn't the number of bugs — it's how fast they find and fix them. This tutorial teaches you R's debugging toolkit.
Introduction
R's debugging workflow has three phases:
Locate — find where the error happens (traceback(), error messages)
Inspect — examine the state at that point (browser(), debug())
Fix — correct the code and verify
Here are the tools:
Tool
What it does
When to use
traceback()
Shows the call stack after an error
First step — "where did it fail?"
browser()
Pauses execution, opens interactive prompt
Insert where you want to inspect
debug(fn)
Auto-inserts browser at the start of fn
Step through a function
debugonce(fn)
Like debug but only for the next call
Quick one-time inspection
options(error = recover)
Opens browser at error location
Catch unexpected errors
traceback(): Where Did It Fail?
After an error, traceback() shows the call stack — the chain of function calls that led to the error:
# Create a chain of functions that will fail
inner <- function(x) {
log(x) # fails if x is character
}
middle <- function(x) {
inner(x)
}
outer <- function(x) {
middle(x)
}
# Catch the error and show the traceback
tryCatch(
outer("not a number"),
error = function(e) {
cat("Error:", conditionMessage(e), "\n\n")
cat("The call stack would be:\n")
cat(" 3: inner(x) -> log(x) fails here\n")
cat(" 2: middle(x) -> called inner\n")
cat(" 1: outer('not a number') -> called middle\n")
}
)
In an interactive session, you'd simply call traceback() right after the error. Read the output bottom-to-top: the bottom is where execution started, the top is where it failed.
browser(): Pause and Inspect
Insert browser() anywhere in your code to pause execution and open an interactive prompt:
# In a real R session, browser() pauses here and lets you type commands:
# n — execute next line
# s — step into function call
# c — continue (resume execution)
# Q — quit the browser
# ls() — see local variables
# any expression — evaluate it
problematic <- function(data) {
# Uncomment browser() in a local R session to pause here:
# browser()
total <- sum(data)
avg <- total / length(data)
# Let's simulate what you'd see in the browser
cat("In a browser session, you could inspect:\n")
cat(" data:", data, "\n")
cat(" total:", total, "\n")
cat(" avg:", avg, "\n")
avg
}
result <- problematic(c(10, 20, NA, 40))
cat("Result:", result, "\n")
cat("Oops! NA because sum() returns NA when input has NA\n")
cat("Fix: use na.rm = TRUE\n")
Conditional browser
# Only pause when a condition is met
analyze <- function(values) {
results <- numeric(length(values))
for (i in seq_along(values)) {
results[i] <- sqrt(values[i])
# In a real session, this would pause only for problem values:
# if (is.nan(results[i])) browser()
if (is.nan(results[i])) {
cat(sprintf("Problem at index %d: sqrt(%g) = NaN\n", i, values[i]))
}
}
results
}
output <- analyze(c(4, 9, -1, 16, -25))
cat("Results:", output, "\n")
debug() and debugonce(): Step Through Functions
debug(fn) marks a function so that every call to it opens the browser. debugonce(fn) does the same but only for the next call:
# In a real R session:
# debug(my_function) — every call will enter browser
# my_function(args) — browser opens, step through with n/s/c
# undebug(my_function) — stop debugging
# debugonce(my_function) — enters browser only on next call
# Simulated example showing what you'd see:
calculate_bmi <- function(weight_kg, height_m) {
bmi <- weight_kg / height_m^2
category <- if (bmi < 18.5) "Underweight"
else if (bmi < 25) "Normal"
else if (bmi < 30) "Overweight"
else "Obese"
list(bmi = round(bmi, 1), category = category)
}
# If you ran: debugonce(calculate_bmi)
# Then: calculate_bmi(70, 1.75)
# You'd step through each line:
# Browse[2]> n (execute: bmi <- 70 / 1.75^2)
# Browse[2]> bmi (inspect: 22.85714)
# Browse[2]> n (execute: category <- ...)
# Browse[2]> category (inspect: "Normal")
# Browse[2]> c (continue to end)
result <- calculate_bmi(70, 1.75)
cat("BMI:", result$bmi, "-", result$category, "\n")
Setting options(error = recover) makes R open a browser session whenever any error occurs — you can inspect the state at each level of the call stack:
# In a real R session:
# options(error = recover)
#
# When an error happens, R shows:
# Enter a frame number, or 0 to exit
# 1: outer("bad")
# 2: middle("bad")
# 3: inner("bad")
#
# You type a number to inspect that environment:
# Selection: 3
# Browse[1]> ls()
# Browse[1]> x
# Browse[1]> 0 (exit)
# Reset to default behavior:
# options(error = NULL)
# Demonstrate the concept:
cat("options(error = recover) workflow:\n")
cat("1. Set: options(error = recover)\n")
cat("2. Run your code\n")
cat("3. When error occurs, R lists the call stack\n")
cat("4. Enter a frame number to inspect that environment\n")
cat("5. Type 0 to exit\n")
cat("6. Reset: options(error = NULL)\n")
Practical Debugging Strategy
Here's a systematic approach to debugging R code:
# Step 1: Read the error message carefully
# Step 2: Use traceback() to find where it happened
# Step 3: Insert browser() just before the failing line
# Step 4: Inspect variables and test fixes interactively
# Step 5: Remove browser() and apply the fix
# Example: debugging a data processing pipeline
process_scores <- function(scores) {
# Step 1: Clean
clean <- as.numeric(scores)
# Step 2: Validate
if (any(is.na(clean))) {
bad_idx <- which(is.na(clean))
cat("Warning: NAs at positions:", bad_idx, "\n")
cat("Original values:", scores[bad_idx], "\n")
clean <- clean[!is.na(clean)]
}
# Step 3: Normalize to 0-100
if (length(clean) == 0) stop("No valid scores after cleaning")
min_score <- min(clean)
max_score <- max(clean)
if (min_score == max_score) {
warning("All scores are identical — returning 50 for all")
return(rep(50, length(clean)))
}
normalized <- (clean - min_score) / (max_score - min_score) * 100
round(normalized, 1)
}
# Test cases that exercise different code paths
cat("Normal:", process_scores(c(60, 70, 80, 90, 100)), "\n")
cat("With bad data:", process_scores(c("80", "ninety", "100", "NA")), "\n")
# Exercise: This function should return the second-largest value
# in a vector. It has a bug. Find and fix it.
second_largest <- function(x) {
sorted <- sort(x)
sorted[length(sorted) - 1]
}
# Test cases:
cat("Test 1:", second_largest(c(5, 3, 8, 1, 9)), "\n") # Should be 8
cat("Test 2:", second_largest(c(1, 1, 1)), "\n") # Should be 1
cat("Test 3:", second_largest(c(10, 5)), "\n") # Should be 5
cat("Test 4:", second_largest(c(7)), "\n") # Should be NA or error
# The bug shows with duplicate maximum values:
cat("Test 5:", second_largest(c(9, 9, 5, 3)), "\n") # Should be 9
# Fix the function below:
Click to reveal solution
```r
# The original function works for simple cases but:
# 1. Returns wrong result for single-element vectors
# 2. Returns correct result for duplicates (which is fine)
# The main bug: no handling of edge cases
second_largest <- function(x) {
if (length(x) < 2) {
warning("Need at least 2 elements for second largest")
return(NA)
}
unique_sorted <- sort(unique(x), decreasing = TRUE)
if (length(unique_sorted) < 2) return(x[1]) # All identical
unique_sorted[2]
}
cat("Test 1:", second_largest(c(5, 3, 8, 1, 9)), "\n") # 8
cat("Test 2:", second_largest(c(1, 1, 1)), "\n") # 1
cat("Test 3:", second_largest(c(10, 5)), "\n") # 5
cat("Test 4:", second_largest(c(7)), "\n") # NA
cat("Test 5:", second_largest(c(9, 9, 5, 3)), "\n") # 9
**Explanation:** The debug approach: (1) identify edge cases, (2) check each test case mentally, (3) add guard clauses for edge cases. Using `sort(unique(x), decreasing = TRUE)` ensures we get the second *distinct* largest value.
Exercise 2: Debug the Pipeline
# Exercise: This pipeline crashes. Use the error message
# to find and fix the bug. Don't change the input data.
process <- function(df) {
df$score_pct <- df$score / df$max_score * 100
df$grade <- ifelse(df$score_pct >= 70, "Pass", "Fail")
df$summary <- paste(df$name, ":", df$grade)
df
}
data <- data.frame(
name = c("Alice", "Bob", "Carol"),
score = c(85, 62, 91),
max_score = c(100, 100, 100)
)
result <- process(data)
print(result)
# Now try with this data — what happens?
# data2 <- data.frame(
# name = c("Alice", "Bob"),
# score = c(85, 62),
# max_score = c(100, 0) # Bug: division by zero
# )
# result2 <- process(data2)
# Fix the process function to handle max_score = 0
**Explanation:** Division by zero in R produces `Inf`, not an error. But `Inf >= 70` is `TRUE`, so Bob would get "Pass" with 0 max score. The fix checks for `max_score == 0` and assigns 0% instead.
Summary
Tool
Usage
When
traceback()
Call after error
Find where error occurred
browser()
Insert in code
Inspect state at a specific point
debug(fn)
Before calling fn
Step through every call to fn
debugonce(fn)
Before calling fn
Step through next call only
undebug(fn)
After debugging
Stop auto-debugging fn
options(error = recover)
Set once
Auto-debug all errors
options(error = NULL)
Reset
Return to normal behavior
Debugging workflow: Error message -> traceback() -> browser() at suspicious line -> inspect variables -> fix -> test.
FAQ
Can I debug code in WebR or R Markdown?
browser() requires an interactive session, so it won't work in WebR or non-interactive R Markdown renders. Use cat() or print() for debugging in those contexts. In RStudio, you can use breakpoints (click in the margin) which work like browser().
What do the browser commands n, s, c, Q mean?
n (next) executes the current line and moves to the next. s (step) steps into a function call. c (continue) resumes normal execution. Q quits the browser and returns to the top level.
How do I debug inside apply/lapply/map?
Insert browser() inside the function you pass to lapply(). Or wrap it in a tryCatch to catch which element fails. Example: lapply(data, function(x) { if (is.character(x)) browser(); process(x) }).
What's Next?
With debugging skills in hand, explore related topics:
R Conditions System — handle errors gracefully with tryCatch
R Common Errors — the 50 most frequent errors and their fixes
R Execution Stack — understand sys.call(), parent.frame() internals