22.5 Non-interactive debugging

Debugging is most challenging when you can’t run code interactively, typically because it’s part of some pipeline run automatically (possibly on another computer), or because the error doesn’t occur when you run same code interactively. This can be extremely frustrating!

This section will give you some useful tools, but don’t forget the general strategy in Section 22.2. When you can’t explore interactively, it’s particularly important to spend some time making the problem as small as possible so you can iterate quickly. Sometimes callr::r(f, list(1, 2)) can be useful; this calls f(1, 2) in a fresh session, and can help to reproduce the problem.

You might also want to double check for these common issues:

  • Is the global environment different? Have you loaded different packages? Are objects left from previous sessions causing differences?

  • Is the working directory different?

  • Is the PATH environment variable, which determines where external commands (like git) are found, different?

  • Is the R_LIBS environment variable, which determines where library() looks for packages, different?

22.5.1 dump.frames()

dump.frames() is the equivalent to recover() for non-interactive code; it saves a last.dump.rda file in the working directory. Later, an interactive session, you can load("last.dump.rda"); debugger() to enter an interactive debugger with the same interface as recover(). This lets you “cheat”, interactively debugging code that was run non-interactively.

# In batch R process ----
dump_and_quit <- function() {
  # Save debugging info to file last.dump.rda
  dump.frames(to.file = TRUE)
  # Quit R with error status
  q(status = 1)
}
options(error = dump_and_quit)

# In a later interactive session ----
load("last.dump.rda")
debugger()

22.5.3 RMarkdown

Debugging code inside RMarkdown files requires some special tools. First, if you’re knitting the file using RStudio, switch to calling rmarkdown::render("path/to/file.Rmd") instead. This runs the code in the current session, which makes it easier to debug. If doing this makes the problem go away, you’ll need to figure out what makes the environments different.

If the problem persists, you’ll need to use your interactive debugging skills. Whatever method you use, you’ll need an extra step: in the error handler, you’ll need to call sink(). This removes the default sink that knitr uses to capture all output, and ensures that you can see the results in the console. For example, to use recover() with RMarkdown, you’d put the following code in your setup block:

options(error = function() {
  sink()
  recover()
})

This will generate a “no sink to remove” warning when knitr completes; you can safely ignore this warning.

If you simply want a traceback, the easiest option is to use rlang::trace_back(), taking advantage of the rlang_trace_top_env option. This ensures that you only see the traceback from your code, instead of all the functions called by RMarkdown and knitr.

options(rlang_trace_top_env = rlang::current_env())
options(error = function() {
  sink()
  print(rlang::trace_back(bottom = sys.frame(-1)), simplify = "none")
})