Debugging R Code — Master R's built-in debugging tools:
Bugs happen. Even experienced R programmers encounter errors that aren’t obvious from the error message alone. Debugging R code effectively means knowing which tool to reach for: traceback() to see the call stack, browser() to pause and inspect state interactively, and debug() to step through a function line by line. This guide covers the essential debugging functions you’ll use daily.
Understanding error messages
When R throws an error, it shows a message but not always the full picture. The first step is getting the call stack.
# A function with a bug
calculate_stats <- function(data) {
mean_value <- mean(data$column)
sd_value <- sd(data$column)
return(list(mean = mean_value, sd = sd_value))
}
# This will fail
calculate_stats(mtcars)
# Error in mean.default(data$column) :
# 'x' must be numeric
The error tells you something is wrong with the column, but not how you got there. R’s error messages report which function failed but don’t show the chain of calls that led there. To trace the path through nested function calls, you need the call stack, which traceback() provides.
Using traceback()
The traceback() function prints the call stack that led to the error. Call it immediately after an error occurs:
calculate_stats(mtcars)
# Error in mean.default(data$column) : 'x' must be numeric
traceback()
# 4: mean.default(data$column) at #2 : calculate_stats.R#2
# 3: sd.default(data$column) at #3 : calculate_stats.R#3
# 2: calculate_stats(mtcars)
# 1: eval(expr, envir, enclos)
The call stack shows the path: your function called mean() and sd(), both failed. Now you know exactly where to look. For deeper inspection, traceback() tells you where but not what state the variables were in. To examine values mid-execution, you need an interactive breakpoint, which browser() provides by pausing execution and opening a REPL inside your function’s environment.
Using browser()
The browser() function pauses execution and drops you into an interactive debugging mode. This is useful when you need to inspect the state of your code at a specific point:
process_data <- function(df) {
browser() # Execution stops here
result <- df$x + df$y
return(result)
}
process_data(data.frame(x = 1:5, y = 6:10))
# Called from: process_data(data.frame(x = 1:5, y = 6:10))
# Browse[1]>
Once inside browser mode, you can:
- Type variable names to print their values
- Use
nto execute the next line - Use
cto continue to the next breakpoint - Use
Qto exit the browser
This gives you X-ray vision into what’s actually happening inside your function.
Using debug() and undebug()
The debug() function marks a function for debugging mode. Every time that function is called, R enters the browser automatically:
debug(calculate_stats)
calculate_stats(mtcars)
# Entering debugger for calculate_stats at r-test.R#1:2
# Browse[1]>
This is powerful for functions you suspect have issues. You can step through the entire function, watching variables change with each line. The debug flag persists across calls, so every invocation of the function triggers the browser until you explicitly turn it off. This makes debug() ideal for functions called repeatedly in loops or apply operations where a one-time breakpoint would be tedious to re-insert.
When you’re done debugging, remove the flag with undebug():
undebug(calculate_stats)
Calling undebug() removes the debugging flag so the function runs normally again. If you lose track of which functions are currently flagged with debug, isdebugged() tells you whether a given function is set to enter the browser on every call.
isdebugged(calculate_stats)
# [1] TRUE
When you don’t know which function to debug in advance, or when an error occurs deep inside a package you didn’t write, options(error = recover) is the right tool. Instead of pre-placing breakpoints, it activates on any error and lets you choose which frame in the call stack to inspect. This is especially handy for errors that only appear after long-running computations where reproducing the error to add a browser() call would waste time.
Using options(error=recover)
Setting options(error=recover) changes R’s behavior when an error occurs. Instead of stopping, R drops you into a browser-like interface with access to all frames in the call stack:
options(error = recover)
calculate_stats(mtcars)
# Error in mean.default(data$column) : 'x' must be numeric
# Enter a frame number, or 0 to exit :
# Selection: 1
# Browse[1 of 4]>
You can examine variables in any frame. This is particularly useful when errors occur deep in nested function calls. Type 0 to exit the debugger. When you’re done investigating and want R to revert to its default behavior, reset the error option to NULL so that future errors stop execution normally instead of entering the recovery browser.
options(error = NULL)
Using warning() and message()
Not all problems are errors. Sometimes you want to inform users about something without stopping execution. R provides two functions for signaling non-fatal conditions: warning() for unexpected but recoverable situations, and message() for purely informational output. Choosing the right one matters because warnings appear in red and can be collected programmatically, while messages print to stderr without the warning signal.
calculate_mean <- function(x) {
if (!is.numeric(x)) {
warning("Converting x to numeric")
x <- as.numeric(x)
}
mean(x)
}
calculate_mean(c(1, 2, 3))
# [1] 2
calculate_mean(c("1", "2", "3"))
# Warning: Converting x to numeric
# [1] 2
The difference: warnings can be suppressed with suppressWarnings(), while messages can be suppressed with suppressMessages(). Use warnings for unexpected but recoverable situations, messages for informational output.
Finding the source of errors
When error messages aren’t clear, work backwards:
- Run
traceback()immediately after an error - Add
browser()before where you think the problem is - Use
debug()to step through the entire function - Check the data types of inputs; R is strict about this
- Look for typos in variable names; R won’t catch these automatically
The key is reducing the search space. Divide your code in half, add a browser statement in the middle, and determine which half contains the bug. Repeat until you isolate the exact line.
Debugging in packages
When debugging code inside a package, devtools::load_all() makes package functions available with their debugging behavior intact. Insert browser() inside a package function, run devtools::load_all(), then call the function. Without load_all(), package functions called via library() run from the compiled bytecode and browser() may not behave as expected. Use trace(pkg::fun, browser) to insert a breakpoint in a loaded package without modifying source files.
The debugging mindset
Debugging is hypothesis testing. When code produces a wrong result or error, form a hypothesis about the cause, design a test to check it, and update your understanding. Random edits are inefficient; systematic investigation converges faster.
Start by reproducing the problem reliably. A minimal reproducible example, the smallest code that exhibits the bug, focuses attention on the cause. Remove everything not needed to reproduce the issue. This process itself often reveals the bug. Understand the difference between where the error is reported and where it originates. R’s error messages often point to a call deep in a package’s internals. Read the full traceback from bottom (where you called) to top (where the error occurred), since the actual bug is usually in your code, not in the package.
RStudio debugger
RStudio provides a visual debugger. Set breakpoints by clicking the gutter (left margin) in the source editor. When a breakpoint is hit, RStudio shows the current environment, call stack, and highlights the current line. Step through code with toolbar buttons or keyboard shortcuts.
The environment pane shows the current environment’s contents, and the call stack panel navigates to any frame in the stack. For complex bugs, the visual debugger is faster than print debugging. Use options(error = recover) to drop into a browser at the point of any error, letting you inspect state in frames where the error occurred. This is especially useful for errors deep in package code you cannot easily trace().