Author: John Mount, Nina Zumel
Date: 2023-08-19

# 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 `i`th 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:

- a random temp file (if `saveDest` is null)
- a user chosen file (if `saveDest` is character)
- a `globalenv()` variable (if `saveDest` is a name, as produced by `as.name()` or `quote()`)
- passed to a user function (if `saveDest` is a function).

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`.