Debugging & Reading Errors
An error message is not an obstacle between you and the result. It is the most specific information you will ever get about what your code actually did, and most people throw it away by reading only the last line.
Every error quoted below is real, produced by a script that raises it on purpose and catches it. Nothing here is written from memory, which matters on this page more than most: an error message typed from recall is wrong often enough to mislead.
The message and the call
Section titled “The message and the call”R prints two things, and people read one of them. Here a count column arrives as text,
which is what read.csv gives you when a cell says n/a:
#' Normalise.#'#' @param counts Input value.#' @param total Input value.#' @return Computed result.Normalise <- function(counts, total) { counts / total}
#' CountsPerMillion.#'#' @param counts Input value.#' @return Computed result.CountsPerMillion <- function(counts) { Normalise(counts, sum(counts)) * 1e6}
counts <- c("120", "455", "89") # character, not numericCountsPerMillion(counts)MESSAGE: invalid 'type' (character) of argumentCALL: sum(counts)The call is the part that gets skipped. It says the failure happened inside sum(),
not on the line you typed. That narrows the search immediately.
Find your own frames in the stack
Section titled “Find your own frames in the stack”traceback() prints the calls behind the last error. In a script you can capture the same
stack at the point of failure:
call stack at the point of failure, innermost last: 1: tryCatch(withCallingHandlers(cpm(counts), error = function(... 2: tryCatchList(expr, classes, parentenv, handlers) 3: tryCatchOne(expr, names, parentenv, handlers[[1L]]) 4: doTryCatch(return(expr), name, parentenv, handler) 5: withCallingHandlers(cpm(counts), error = function(e) { ... 6: cpm(counts) <- your code 7: normalise(counts, sum(counts)) <- your code 8: .handleSimpleError(function (e) { stack <- sys.calls()... 9: h(simpleError(msg, call))Nine frames. Two are yours. Everything else is tryCatch machinery and condition
handling, and learning to skip it is the actual skill. Scan for your own function names,
ignore the rest, and start at the innermost one of yours.
Then work upward. normalise() is where R gave up, and nothing in normalise is
wrong: it was handed a bad value. cpm() passed it along. The bug is on the line that
built counts, which appears in no frame at all. The deepest frame is rarely the culprit.
str() is the highest-value call in R
Section titled “str() is the highest-value call in R”The error above was the lucky case, because it raised. str() tells you what an object
is rather than what you assumed:
str(counts) chr [1:3] "120" "455" "89"Three characters, not three numbers. Reach for str() before you reach for anything else;
on a list or an S4 object it is the difference between guessing and knowing.
A warning is more dangerous than an error
Section titled “A warning is more dangerous than an error”An error stops. A warning lets the wrong number keep moving:
as.numeric(c("120", "455", "not_measured"))WARNING: NAs introduced by coercionresult: 120, 455, NAmean(result) = NAThe script carried on and produced NA. In a longer pipeline that NA propagates
silently into a summary, a model, or a figure.
While debugging, promote warnings to errors so they stop the script where they happen:
options(warn = 2)Fail early with stopifnot()
Section titled “Fail early with stopifnot()”Move the failure from two frames down to the front door of the function, with a message that names the broken assumption:
#' CountsPerMillion.#'#' @param counts Input value.#' @return Computed result.CountsPerMillion <- function(counts) { stopifnot( "counts must be numeric" = is.numeric(counts), "counts must not be negative" = all(counts >= 0, na.rm = TRUE) ) counts / sum(counts) * 1e6}MESSAGE: counts must be numericNamed conditions became the message. Without names you get the expression back, which is less useful when someone else hits it.
Catch the condition you expected
Section titled “Catch the condition you expected”Worth looking for in any code you are reviewing, including your own, because a broad catch is a tempting way to make a script stop complaining:
result <- tryCatch(run_analysis(df), error = function(e) NULL)That swallows every error, including the typo three lines in, and leaves you a NULL you
cannot explain. Catch the specific failure you know how to recover from and let the rest
crash. A crash is information.
#' ReadOne.#'#' @param path Input value.#' @return Computed result.ReadOne <- function(path) { tryCatch( read.csv(path), warning = function(w) { cat("could not read ", path, ": ", conditionMessage(w), "\n", sep = "") NULL } )}The interactive debuggers
Section titled “The interactive debuggers”These need a human at the console, so the script behind this page cannot run them. They are how you inspect a live frame instead of reasoning about it.
| Tool | What it does |
|---|---|
browser() |
drop into a prompt at that line |
debug(fn) / debugonce(fn) |
enter the debugger on every call to fn, or once |
trace(fn, browser) |
inject a breakpoint without editing the source |
options(error = recover) |
pick a frame to inspect after any error |
At a browser() prompt: n next, s step in, c continue, Q quit, and any R
expression evaluates inside the paused frame.
Quick reference
Section titled “Quick reference”| Task | Call |
|---|---|
| What is this object really | str(x), class(x), typeof(x) |
| Where did the error come from | traceback() |
| Stop at a line | browser() |
| Debug a function | debugonce(fn) |
| Inspect after any error | options(error = recover) |
| Turn warnings into errors | options(warn = 2) |
| Guard an input | stopifnot("message" = condition) |
| Handle one condition | tryCatch(expr, error = function(e) ...) |
Next steps
Section titled “Next steps”- Testing & Assertions, the other half of this: a guard catches a bad input, a test catches a bad implementation.
- Functions, including the scope rules behind “it worked in my session”.
- Fundamentals You Still Need, if you work with a coding agent: what to check in what it produces, and why this page is on that list.
The runnable script and the container are in the companion code repo under
guides/programming/r/debugging/.