22.4 Interactive debugger

Sometimes, the precise location of the error is enough to let you track it down and fix it. Frequently, however, you need more information, and the easiest way to get it is with the interactive debugger which allows you to pause execution of a function and interactively explore its state.

If you’re using RStudio, the easiest way to enter the interactive debugger is through RStudio’s “Rerun with Debug” tool. This reruns the command that created the error, pausing execution where the error occurred. Otherwise, you can insert a call to browser() where you want to pause, and re-run the function. For example, we could insert a call browser() in g():

g <- function(b) {
  browser()
  h(b)
}
f(10)

browser() is just a regular function call which means that you can run it conditionally by wrapping it in an if statement:

g <- function(b) {
  if (b < 0) {
    browser()
  }
  h(b)
}

In either case, you’ll end up in an interactive environment inside the function where you can run arbitrary R code to explore the current state. You’ll know when you’re in the interactive debugger because you get a special prompt:

Browse[1]> 

In RStudio, you’ll see the corresponding code in the editor (with the statement that will be run next highlighted), objects in the current environment in the Environment pane, and the call stack in the Traceback pane.

22.4.1 browser() commands

As well as allowing you to run regular R code, browser() provides a few special commands. You can use them by either typing short text commands, or by clicking a button in the RStudio toolbar, Figure 22.1:

RStudio debugging toolbar

Figure 22.1: RStudio debugging toolbar

  • Next, n: executes the next step in the function. If you have a variable named n, you’ll need print(n) to display its value.

  • Step into, or s: works like next, but if the next step is a function, it will step into that function so you can explore it interactively.

  • Finish, or f: finishes execution of the current loop or function.

  • Continue, c: leaves interactive debugging and continues regular execution of the function. This is useful if you’ve fixed the bad state and want to check that the function proceeds correctly.

  • Stop, Q: stops debugging, terminates the function, and returns to the global workspace. Use this once you’ve figured out where the problem is, and you’re ready to fix it and reload the code.

There are two other slightly less useful commands that aren’t available in the toolbar:

  • Enter: repeats the previous command. I find this too easy to activate accidentally, so I turn it off using options(browserNLdisabled = TRUE).

  • where: prints stack trace of active calls (the interactive equivalent of traceback).

22.4.2 Alternatives

There are three alternatives to using browser(): setting breakpoints in RStudio, options(error = recover), and debug() and other related functions.

22.4.2.1 Breakpoints

In RStudio, you can set a breakpoint by clicking to the left of the line number, or pressing Shift + F9. Breakpoints behave similarly to browser() but they are easier to set (one click instead of nine key presses), and you don’t run the risk of accidentally including a browser() statement in your source code. There are two small downsides to breakpoints:

  • There are a few unusual situations in which breakpoints will not work. Read breakpoint troubleshooting for more details.

  • RStudio currently does not support conditional breakpoints.

22.4.2.2 recover()

Another way to activate browser() is to use options(error = recover). Now when you get an error, you’ll get an interactive prompt that displays the traceback and gives you the ability to interactively debug inside any of the frames:

options(error = recover)
f("x")
#> Error: `d` must be numeric
#> 
#> Enter a frame number, or 0 to exit   
#> 
#> 1: f("x")
#> 2: debugging.R#1: g(a)
#> 3: debugging.R#2: h(b)
#> 4: debugging.R#3: i(c)
#> 
#> Selection:

You can return to default error handling with options(error = NULL).

22.4.2.3 debug()

Another approach is to call a function that inserts the browser() call for you:

  • debug() inserts a browser statement in the first line of the specified function. undebug() removes it. Alternatively, you can use debugonce() to browse only on the next run.

  • utils::setBreakpoint() works similarly, but instead of taking a function name, it takes a file name and line number and finds the appropriate function for you.

These two functions are both special cases of trace(), which inserts arbitrary code at any position in an existing function. trace() is occasionally useful when you’re debugging code that you don’t have the source for. To remove tracing from a function, use untrace(). You can only perform one trace per function, but that one trace can call multiple functions.

22.4.2.4 Call stack

Unfortunately, the call stacks printed by traceback(), browser() & where, and recover() are not consistent. The following table shows how the call stacks from a simple nested set of calls are displayed by the three tools. The numbering is different between traceback() and where, and recover() displays calls in the opposite order.

traceback() where recover() rlang functions
5: stop("...")
4: i(c) where 1: i(c) 1: f() 1. └─global::f(10)
3: h(b) where 2: h(b) 2: g(a) 2. └─global::g(a)
2: g(a) where 3: g(a) 3: h(b) 3. └─global::h(b)
1: f("a") where 4: f("a") 4: i("a") 4. └─global::i("a")

RStudio displays calls in the same order as traceback(). rlang functions use the same ordering and numbering as recover(), but also use indenting to reinforce the hierarchy of calls.

22.4.3 Compiled code

It is also possible to use an interactive debugger (gdb or lldb) for compiled code (like C or C++). Unfortunately that’s beyond the scope of this book, but there are a few resources that you might find useful: