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.