Debug Vignette

John Mount, Nina Zumel

2023-08-19

This vignette demonstrates debugging a user-created function with the DebugFnW call. For our example, we will use a simple function that takes an argument i and returns the ith index of a ten-element vector:

# load package
library("wrapr")

# user function
f <- function(i) { (1:10)[[i]] }

Let’s imagine that we are calling this function deep within another process; perhaps we are calling it repeatedly, on a long sequence of (possibly unknown to us) inputs.

inputs = c(4,5,2,9,0,8)

tryCatch(
  for(x in inputs) {
     f(x)
  },
  error = function(e) { print(e) })
## <simpleError in (1:10)[[i]]: attempt to select less than one element in get1index <real>>

Oops! We’ve crashed, and if this loop were deep in another process, we wouldn’t know why, or where. If we suspect that the function f is the cause, then we can wrap f using wrapr:DebugFn.

DebugFnW(saveDest, fn) wraps its function argument fn, captures any arguments that cause it to fail, and saved those arguments and other state to a specified destination saveDest.

The state data is written to:

Here, we wrap f and save error state into the global variable lastError.

# wrap function with writeBack
df <- DebugFnW(as.name('lastError'), f)

Now we run the same loop as above, with the wrapped function df (note that the tryCatch is not strictly needed, this is just for running this example in a vignette).

# capture error (Note: tryCatch not needed for user code!)
tryCatch(
  for(x in inputs) {
     df(x)
  },
  error = function(e) { print(e) })
## <simpleError in value[[3L]](cond): wrapr::DebugFnW: wrote error to globalenv() variable 'lastError'
##  You can reproduce the error with:
##  'do.call(p$fn, p$args)' (replace 'p' with actual variable name)>

We can then examine the error. Note in particular that lastError$fn_name records the name of the function that crashed, and lastError$args records the arguments that the function was called with. Also in these examples we are wrapping our code with a tryCatch block to capture exceptions; this is only to allow the knitr sheet to continue and not needed to use the debugging wrappers effectively.

# examine error
str(lastError)
## List of 4
##  $ fn       :function (i)  
##   ..- attr(*, "srcref")= 'srcref' int [1:8] 5 6 5 32 6 32 5 5
##   .. ..- attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x7fd4452ee520> 
##  $ args     :List of 1
##   ..$ : num 0
##  $ namedargs: language df(x)
##  $ fn_name  : chr "f"
lastError$args
## [[1]]
## [1] 0

In many situations, just knowing the arguments is enough information (“Oops, we tried to index the vector from zero!”). In more complicated cases, we can set a debug point on the offending function, and then call it again with the failing arguments in order to track down the bug.

# redo call, perhaps debugging
tryCatch(
  do.call(lastError$fn_name, lastError$args),
  error = function(e) { print(e) })
## <simpleError in (1:10)[[i]]: attempt to select less than one element in get1index <real>>
# clean up
rm(list='lastError')

In many cases you may prefer to save the failing state into an external file rather than into the current runtime environment. Below we show example code for saving state to an RDS file.

saveDest <- paste0(tempfile('debug'),'.RDS')
# wrap function with saveDeest
df <- DebugFnW(saveDest,f)
# capture error (Note: tryCatch not needed for user code!)
tryCatch(
  for(x in inputs) {
    df(x)
  },
  error = function(e) { print(e) })

We can later read that file back into R, for debugging.

# load data
lastError <- readRDS(saveDest)
# examine error
str(lastError)
# redo call, perhaps debugging
tryCatch(
  do.call(lastError$fn_name, lastError$args),
  error = function(e) { print(e) })
# clean up
file.remove(saveDest)

For more practice, please view our video on wrapper debugging.

Note: wrapr debug functionality rehashes some of the capabilities of dump.frames (see help(dump.frames)). Roughly dump.frames catches the exception (so trying to step or continue re-throws, and arguments may have moved from their starting values) and wrapr catches the call causing the exception in a state prior to starting the calculation (so arguments should be at their starting values). We have found some cases where wrapr is a bit more convenient in how it interacts with the RStudio visual debugger (please see this screencast for some comparison). Also, please see this article for use of tryCatch and withRestarts.