Advanced R Package Development
traceback()browser(): interactive debuggingdebug(), and debugonce()rlang::last_trace()When something goes wrong, R prints an error. The first step is always to read it carefully.
The error tells you what went wrong, but not where in the call chain.
3: validate_numeric_inputs(catch = catch, effort = effort) at cpue.R#5
2: cpue(catch, effort, ...) at biomass.R#6
1: biomass_index(area_swept = 5, catch = "bad", effort = 10)
cpue().validate_numeric_inputs() β where the error was raised.The quick-and-dirty approach: add a print() or message() to see what is happening at a given point.
biomass_index <- function(cpue = NULL, area_swept, catch = NULL, effort = NULL, ...) {
rlang::check_dots_used()
if (is.null(cpue) && (!is.null(catch) && !is.null(effort))) {
cpue <- cpue(catch, effort, ...)
}
validate_numeric_inputs(cpue = cpue, area_swept = area_swept)
print(paste("cpue value:", cpue)) # <-- temporary debugging output
cpue * area_swept
}Once you have found the problem, delete the print() statement.
if statement runsWhen you need more β complex logic, many intermediate values, full environment inspection β reach for browser().
browser() pauses execution and drops you into an interactive debugger:
cpue <- function(
catch,
effort,
gear_factor = 1,
method = c("ratio", "log"),
verbose = getOption("fishr.verbose", FALSE)
) {
method <- match.arg(method)
validate_numeric_inputs(catch = catch, effort = effort)
browser() # <-- execution pauses here
raw_cpue <- switch(method, ratio = catch / effort, log = log(catch / effort))
raw_cpue * gear_factor
}Once inside the browser you have a mini command language:
| Command | Action |
|---|---|
n |
Next: execute the current line and step to the next |
s |
Step into: if the current line calls a function, enter it |
c |
Continue: resume until the next browser() or end |
Q |
Quit: stop execution immediately |
where |
Show the call stack (like traceback() while paused) |
You can also run any R expression: ls(), str(catch), etc.
The same commands appear as buttons in RStudio when you enter debugging mode.
You donβt always want to pause on every call. Stop only when something is wrong:
Run the function as many times as needed β it only stops when the suspicious condition is met.
Warning
browser() calls should never be committed to your package.
R CMD check (via check()) will warn about leftover browser() calls, but build the habit of removing them immediately after debugging.
Click in the editor gutter (the grey margin to the left of line numbers) to set a breakpoint (red dot).
Same effect as browser() β but does not modify your source file.
After setting a breakpoint, run load_all() to activate it.
For functions you canβt easily edit (other packages, or when you donβt want to touch source):
Breakpoints: your own package code during development debugonce(): quick one-off inspection of any function debug(): step through a function repeatedly (remember undebug())
Post-mortem debugging β inspect state after an error:
R prints the call stack as a numbered list and prompts you to pick a frame:
Enter a frame number, or 0 to exit
1: biomass_index(cpue = "ten", area_swept = 5)
2: validate_numeric_inputs(cpue = cpue, area_swept = area_swept)
Selection:
When working with tidyverse or rlang-based packages, rlang::last_trace() gives a cleaner backtrace than traceback():
rlang::last_trace() organises the call stack as a tree and collapses internal package machinery, making it easier to see the path through your code.
Returns the error object itself β message, call, and (for rlang errors) the backtrace attached to the error.
Useful when working with tidyverse internals or rlang-style errors. Not essential for everyday debugging.
traceback() β find where in the call chain the error occurredbrowser() β interactive inspection for complex logicdebugonce() / debug() β step through functions from other packagesoptions(error = recover) β post-mortem exploration of the full call stackThere is a bug in this standardize_effort() function. It is supposed to convert effort from hours to days (dividing by 24), then calculate CPUE.
Add this to R/utils.R:
The result should be 50 but returns something much smaller. Use the debugging tools you just learned to find and fix it.
The bug is on the conversion line β it multiplies by 24 instead of dividing:
You could find this by:
browser() before the cpue() call and checking effort_daysmessage("effort_days: ", effort_days) to see the intermediate valuedebugonce(standardize_effort) and inspecting each line