callr 3.3.0

  r-lib, callr

  Gábor Csárdi

We have just updated the callr package to version 3.3.0 on CRAN. The biggest change in this release is better support for debugging the background process. See the full changelog here.

callr helps with running R code in a separate R process, synchronously or asynchronously. With synchronous execution the main R process waits until the separate R subprocess finishes, see callr::r(). Asynchronous execution uses processx processes, see callr::r_bg() and callr::r_process() for one-off and callr::r_session() for persistent background R processes.

callr error objects

Debugging code running in a background process is notoriously difficult. Most of the time you cannot use an interactive debugger, and often even print-debugging, i.e. inserting print() and cat() calls into the code that runs in the background, can be non-trivial.

The new 3.3.0 version of callr aims to help with this, by creating better error messages and error traces for errors originating from the background process. In particular, callr now always throws error objects that contain:

  • the exit status of the R process, if the process terminated,
  • the full error object thrown in the subprocess,
  • the call that generated the error,
  • the process id of the subprocess, and
  • the full stack trace in the subprocess.

Here is an example for a trivial error that shows how to extract this information if the error was caught in the main process:

err <- tryCatch(
  callr::r(function() library(Callr)),
  error = function(e) e)
err
#> <callr_status_error: callr subprocess failed: there is no package called ‘Callr’>
#>  in process 
#> -->
#> <callr_remote_error in library(Callr): there is no package called ‘Callr’>

The error objects has two parts. The first is the error object thrown in the main process, and the second is the error object from the the subprocess. We can extract more information from err:

err$status
#> [1] 0
err$parent
#> <callr_remote_error in library(Callr): there is no package called ‘Callr’>
err$parent$call
#> function() library(Callr)
err$parent$`_pid`
#> [1] 79124

err$status is the exit status of the subprocess. This is not present for persistent background processes, i.e. the ones created by r_session, because these do not exit on error, but continue running. err$parent is the error object, thrown in the subprocess. err$parent$call is the call that generated the error, and err$parent$`_pid` is the process id of the subprocess.

The stack trace of the error in subprocess can be printed via err$parent$trace. By default the trace omits the boilerplate frames added by callr, these are usually not very useful for the user. Nevertheless they are still included in err$parent$trace$calls.

err$parent$trace
#> 
#>  ERROR TRACE for packageNotFoundError
#> 
#>  12. (function ()  ...
#>  13. base:::library(Callr)
#>     R/<text>:2:12
#>  14. base:::stop(packageNotFoundError(package, lib.loc, sys.call()))
#>  15. (function (e)  ...
#> 
#>  x there is no package called ‘Callr’ 

The trace starts with the anonymous function that we passed to callr::r(), and it is annotated with package names and source references, if they are available.

The last error

Often, the error object is uncaught, i.e. we don’t tryCatch() the error in the main R process. Then the error message is printed, but the actual error object is lost, and you need to re-run the code in a tryCatch(), hoping that it would produce the same error.

For a better workflow, whenever a callr error is uncaught, callr assigns it to the .Last.error variable, that can be inspected. Of course, a subsequent callr error will overwrite .Last.error, it works very much like .Last.value, but for errors. Here is the same code as above but without the tryCatch():

callr::r(function() library(Callr))
#> Error: callr subprocess failed: there is no package called ‘Callr’
.Last.error
#> <callr_status_error: callr subprocess failed: there is no package called ‘Callr’>
#> -->
#> <callr_remote_error in library(Callr): there is no package called ‘Callr’>
.Last.error$parent$call
#> function() library(Callr)

The last error trace

If the error is uncaught, then callr adds a trace to the error object of the main process as well. The trace will have two parts in this case. callr also sets the .Last.error.trace variable for convenience, this is easier to type than .Last.error$trace.

.Last.error.trace
#> 
#>  ERROR TRACE for callr_status_error, callr_error, rlib_error
#> 
#>  Process 79108:
#>  30. callr::r(function() library(Callr))
#>  31. callr:::get_result(output = out, options)
#>     R/eval.R:149:3
#>  32. base:::throw(new_callr_error(output, msg), parent = err[[2]])
#>     R/result.R:73:5
#> 
#>  x callr subprocess failed: there is no package called ‘Callr’ 
#> 
#>  Process 79135:
#>  44. (function ()  ...
#>  45. base:::library(Callr)
#>     R/<text>:1:10
#>  46. base:::stop(packageNotFoundError(package, lib.loc, sys.call()))
#>  47. (function (e)  ...
#> 
#>  x there is no package called ‘Callr’ 

The top part of the trace contains the frames in the main process, and the bottom part contains the frames in the subprocess, starting with the anonymous function.