Chapter 8 Debugging and Testing
Debugging is the process of reducing a failure to a reproducible cause. Testing turns the expected behavior into checks that detect future regressions.
8.1 Conditions in R
R communicates problems through conditions:
- A message reports progress or context.
- A warning signals a concern while allowing evaluation to continue.
- An error stops the current evaluation.
A function should fail early when continuing would produce a misleading result.
8.2 Capture expected failures
tryCatch() handles a condition deliberately. Do not use it merely to hide an
unexpected error.
captured_message <- tryCatch(
safe_log(c(1, -1)),
error = function(condition) conditionMessage(condition)
)
captured_message
#> [1] "x must contain positive finite numbers."The handler converts this intentionally demonstrated error into a value, so the book can test the expected message without globally allowing failures.
8.3 A reproducible debugging workflow
- Record the exact error and call that triggered it.
- Reduce the input to the smallest example that still fails.
- Inspect structure with
str(),class(),typeof(),length(), andnames(). - Check assumptions at the boundary where the data enter the function.
- Fix the cause and add a test that would have caught it.
unexpected_input <- data.frame(
amount = c("10", "20", "unknown")
)
str(unexpected_input)
#> 'data.frame': 3 obs. of 1 variable:
#> $ amount: chr "10" "20" "unknown"The structure reveals that amount is character. Converting blindly would
turn the unexpected label into a missing value, so validate allowed text first.
8.4 Interactive tools
Use these tools during an interactive debugging session:
traceback()displays the call stack after an error.browser()pauses inside a function.debugonce(function_name)opens the debugger on the next call.recover()lets you inspect active call frames after an error.
Do not leave global debugging options enabled in reproducible scripts.
8.5 Lightweight assertions
stopifnot() is useful for internal invariants and compact checks.
normalize_weights <- function(weights) {
stopifnot(
is.numeric(weights),
length(weights) > 0L,
all(is.finite(weights)),
all(weights >= 0),
sum(weights) > 0
)
weights / sum(weights)
}
normalized_weights <- normalize_weights(c(1, 2, 1))
stopifnot(abs(sum(normalized_weights) - 1) < 1e-12)
normalized_weights
#> [1] 0.25 0.50 0.25For a larger package or application, use a testing framework such as
testthat so tests are organized, named, and run automatically.