Debugging R (browser, traceback)

Reviewed & published by Brayan K

Debugging is the disciplined process of finding and fixing errors in your code, and R gives you a toolkit — traceback, browser, debug, and tryCatch — to inspect the call stack, pause execution mid-run, and recover gracefully from failures.

Part of the free R course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

By the end of this lesson you'll read a traceback, set breakpoints with browser(), step through a function with debug(), and protect code from crashing with tryCatch().

What You'll Learn in This Lesson

1️⃣ Reading the Call Stack with traceback()

When an error fires, the first question is "where did it actually break?" traceback() prints the chain of calls that led to the error. Read it bottom-up: the bottom is your original call, the top is the function that failed.

# When an error fires, traceback() shows the CALL STACK that led to it —
# which function called which, all the way down to the line that broke.

outer <- function(x) inner(x)
inner <- function(x) sqrt(x) + "oops"   # can't add a string to a number

outer(9)
# Error in sqrt(x) + "oops" : non-numeric argument to binary operator

traceback()
# 2: inner(x) at #1
# 1: outer(9)
# Read it bottom-up: outer(9) called inner(x), and inner is where it failed.

# In RStudio, the same stack appears automatically in the console/Traceback pane.

2️⃣ Pausing and Stepping: browser() and debug()

To watch your code run, drop browser() on a line and it pauses there, opening a Browse> prompt where you inspect variables and step with n (next) and c (continue). To step through an entire function without editing it, use debug() or, more often, debugonce().

# browser() pauses execution at that exact line and drops you into an
# interactive session where you can inspect everything mid-run.

risky <- function(values) {
  total <- 0
  for (v in values) {
    browser()          # execution STOPS here on the first iteration
    total <- total + v
  }
  total
}

risky(c(10, 20, 30))

# At the Browse> prompt you can type:
#   v            print the current value
#   total        inspect the running total
#   ls()         list everything in scope
#   n            run the NEXT line
#   c            continue until the next browser()/breakpoint
#   Q            quit the debugger
# debug(f) flags a function so R enters the browser EVERY time it runs.
# debugonce(f) does the same but for the next call only — usually what you want.

clean <- function(x) {
  x <- x[!is.na(x)]        # drop NAs
  x / sum(x)               # normalize
}

debugonce(clean)           # arm the debugger for ONE call
clean(c(1, 2, NA, 3))      # R steps into the function line by line

# Step with 'n', inspect variables by typing their names, 'c' to finish.
# Turn off a permanent debug with:  undebug(clean)

# In RStudio you can also click the LEFT MARGIN of a line to set a
# breakpoint visually — no code change needed.

3️⃣ Recovering Gracefully with tryCatch()

Sometimes you don't want an error to stop everything. tryCatch() wraps risky code with handlers for error and warning, plus a finally block that always runs — so a single bad input becomes an NA and a log message instead of a crash.

Your turn. Use the debugger to find the bug, then apply the fix and run it.

# 🎯 YOUR TURN — fix the bug using debugging tools, then run it.

# This function should average the non-missing values, but it returns NA.
average_clean <- function(x) {
  # Bug: mean() returns NA when x has NAs unless we tell it to skip them.
  mean(x, na.rm = ___)   # 👉 replace ___ with TRUE
}

# Strategy:
#   1) Run  debugonce(average_clean)  then  average_clean(c(4, NA, 8))
#   2) At the Browse> prompt, type  x  to see the NA is present
#   3) Type  mean(x)  -> NA, then  mean(x, na.rm = TRUE) -> 6
#   4) Apply the fix above, re-run.

average_clean(c(4, NA, 8))

# 👉 After the fix this should print 6 instead of NA.

Mini-Challenge: Harden a Parser

Combine the tools: use tryCatch() to survive junk input, message() to report problems, and the recover option to investigate interactively.

# 🎯 MINI-CHALLENGE: harden a parsing function.
#
# parse_nums() should turn a vector of strings into numbers, but some
# entries are junk ("12", "oops", "7"). Make it ROBUST:
#
# 1. Wrap as.numeric() in a per-element tryCatch so bad strings don't
#    crash the loop — return NA for them instead.
# 2. Print a message() naming each value that failed to parse.
# 3. Test with  parse_nums(c("12", "oops", "7"))  -> 12, NA, 7
#
# Bonus: add  options(warn = 2)  temporarily to turn warnings into
#        errors so they surface in traceback() while you investigate.
#
# Then explore: set options(error = recover) and trigger an error to
# pick which frame to inspect interactively (turn it off afterward).

# your code here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Debugging

TaskR Syntax
Show call stacktraceback()
Pause at a linebrowser()
Step through next calldebugonce(f)
Stop debugging a fnundebug(f)
Recover from errorstryCatch(expr, error = ...)
Interactive post-mortemoptions(error = recover)

🎉 Lesson Complete!

Practice quiz

What does traceback() show after an error?

  • The value of every variable
  • A fixed version of your code
  • The call stack of functions that led to the error
  • The runtime in seconds

Answer: The call stack of functions that led to the error. traceback() prints the chain of calls active when the error occurred.

How should you read a traceback?

  • Bottom-up: bottom is the original call, top is where it failed
  • Top-down: top is the original call
  • Left to right
  • Only the middle line matters

Answer: Bottom-up: bottom is the original call, top is where it failed. The bottom is your call; the top is the function that actually failed.

What does browser() do when execution reaches it?

  • Opens a web browser
  • Prints all variables and continues
  • Restarts the function
  • Pauses and opens an interactive prompt to inspect state

Answer: Pauses and opens an interactive prompt to inspect state. browser() halts at that line and lets you inspect variables live.

At the Browse> prompt, what does typing n do?

  • Quits the debugger
  • Runs the next line
  • Names a variable
  • Negates a value

Answer: Runs the next line. n steps to the next line; c continues, Q quits.

How does debugonce(f) differ from debug(f)?

  • It enters the debugger only on the next call, then disarms
  • It debugs every call until undebug()
  • It deletes the function
  • It runs the function twice

Answer: It enters the debugger only on the next call, then disarms. debugonce() is one-shot; debug() persists until undebug(f).

Which call stops debug(f) from triggering on every future call?

  • stopdebug(f)
  • debugoff(f)
  • undebug(f)
  • rm(f)

Answer: undebug(f). undebug(f) removes the persistent debug flag set by debug(f).

What is tryCatch() primarily for?

  • Stepping through code line by line
  • Recovering gracefully from errors and warnings
  • Printing the call stack
  • Setting breakpoints

Answer: Recovering gracefully from errors and warnings. tryCatch() handles conditions so a script can keep running.

In tryCatch(), when does the finally block run?

  • Only on success
  • Only on error
  • Never, it is optional syntax
  • Always, whether or not an error occurred

Answer: Always, whether or not an error occurred. finally always runs after the expression and handlers.

Why call traceback() immediately after an error?

  • It only works once per session
  • A later command overwrites the recorded stack
  • It needs internet access
  • It modifies your variables

Answer: A later command overwrites the recorded stack. traceback() reports the most recent error, so run it before anything else.

What does options(error = recover) enable?

  • Automatic bug fixing
  • Suppression of all errors
  • An interactive post-mortem to pick a frame to inspect
  • Faster execution

Answer: An interactive post-mortem to pick a frame to inspect. On error it lets you choose a stack frame and inspect it; reset with options(error = NULL).

Continue this course

Frequently asked questions

What does traceback() actually show me?

After an error, traceback() prints the call stack — the chain of function calls that was active when the error occurred. It is listed with the most recent call at the top and the original call at the bottom, so reading bottom-up tells the story of how you got there. This instantly answers 'which function actually failed, and who called it?', which is invaluable when an error surfaces deep inside nested or third-party functions. In RStudio the same information appears in the Traceback pane automatically.

How is browser() different from just adding print() statements?

print() debugging shows you a snapshot of one value and requires editing, re-running, and cleaning up afterward. browser() pauses execution at that line and hands you a live interactive prompt where you can inspect any variable, run arbitrary R, step forward line by line with n, or continue with c. It is far more powerful for understanding state mid-run, because you explore the actual environment rather than guessing in advance which values to print.

When should I use debug() versus debugonce()?

debug(f) marks a function so R drops into the debugger every single time that function is called, until you call undebug(f). That is useful when a bug appears intermittently across many calls. debugonce(f) arms the debugger for just the next call and then disarms itself — which is what you want most of the time, since it avoids the annoyance of forgetting to undebug and getting stopped on every subsequent call.

What is the point of tryCatch() if I'm trying to find bugs?

tryCatch() is less about finding bugs and more about handling failures gracefully in code that must keep running — a loop over many files, a web request, a parsing step where some inputs are malformed. You supply handlers for error and warning conditions, plus an optional finally block that always runs. Instead of the whole script aborting on the first bad input, you can log the problem, substitute a sensible default like NA, and continue. For interactive bug-hunting you'd reach for browser() and traceback(); for resilient production code you reach for tryCatch().