Reading an R Error Message
An R error has two parts, and both are useful:
Error in "10" + 5 : non-numeric argument to binary operator
After Error in comes the call - the exact piece of code that failed ("10" + 5). After the colon comes the condition - what went wrong (non-numeric argument to binary operator). Read the call first: it tells you where, and very often you can already see the problem right there in the quoted code. Then read the condition for the why.
Two habits separate people who debug quickly from people who suffer. First, actually read the message - R's messages are usually precise, just tersely worded. Second, debug the first error, not the last: one failure early in a script cascades into a pile of downstream "object not found" errors that all vanish when you fix the original. The rest of this page is a decoder for the messages you'll meet most, then the tools for when reading isn't enough.
The Name Errors: Not Found
Error: object 'total' not found - R searched every environment it knows and no variable has that name. Three causes cover nearly every case:
- A typo, including case. R is case-sensitive:
Total,totalandTOTALare three different names, and R won't guess which one you meant. - The defining line hasn't run. You wrote
total <- sum(x)in the script but never executed it in this session - common after restarting R, where the script file still shows the line but the session has never seen it. Run the script from the top. - Wrong environment. Variables created inside a function live and die inside that call. Using one outside the function is asking for something that no longer exists - return the value instead.
Error: could not find function "read_excel" - same idea, but for a function name. Nine times out of ten the function lives in a package you installed but didn't load this session:
library(readxl) # the fix: loading is per-session, installing is per-machine
df <- read_excel("data.xlsx")
If library(readxl) itself errors, the package isn't installed - install.packages("readxl") first. And if the function is base R, you've typo'd it (lenght() is everyone's rite of passage).
Syntax Errors: unexpected symbol
Error: unexpected symbol in "..." (and its cousins unexpected ')', unexpected string constant) means R couldn't even parse the code. The message points at where R noticed, which is often past where the actual mistake is. The usual suspects:
mean(x na.rm = TRUE) # missing comma - should be mean(x, na.rm = TRUE)
name <- "Ada # unclosed quote - swallows the following lines
total <- sum(c(1, 2, 3) # unclosed paren - the error fires lines later
When the flagged line looks innocent, the mistake is almost always above it: an unclosed quote, parenthesis or brace earlier in the file. A code editor that highlights matching pairs finds these in seconds.
Type and Indexing Errors, Decoded
non-numeric argument to binary operator - you did math on something that isn't a number, usually a number that arrived as text (imports are the classic source - a column with one stray word comes in as character, as covered in data types). The broken version:
x <- "10"
x + 5
# Error in x + 5 : non-numeric argument to binary operator
And the fix - convert, then calculate:
subscript out of bounds - you asked for position n in something with fewer than n elements, with [[ ]]:
scores <- list(ada = 92, grace = 88)
scores[[3]]
# Error in scores[[3]] : subscript out of bounds
Check length() before indexing, or better, ask by name (scores[["grace"]]) so reordering can't break you. Note the asymmetry: single brackets are more forgiving - out-of-range [ ] on a vector quietly returns NA instead of erroring, which trades a loud bug for a silent one.
$ operator is invalid for atomic vectors - $ belongs to lists and data frames. On a named vector, use brackets:
([[ ]] gives the bare value; [ ] keeps the name attached.) This error often means something upstream returned a vector when you expected a data frame - go check that assumption rather than just swapping the operator.
argument is of length zero - an if () received a condition with nothing in it, almost always a NULL that snuck in from a missing list element or a function that returned nothing:
threshold <- NULL
if (threshold > 5) print("big")
# Error in if (threshold > 5) print("big") : argument is of length zero
Guard the check - and note that && stops evaluating as soon as the answer is known, so the comparison never runs on a NULL:
(A related error, missing value where TRUE/FALSE needed, is the same failure with NA instead of NULL - the guard there is is.na(), covered in missing values.)
replacement has length zero - the assignment version of the same disease: x[2] <- numeric(0) tries to fill one slot with zero values. Whatever produced the right side came back empty; debug that, not the assignment.
Warnings Are Not Errors - Which Is the Danger
An error stops execution; a warning doesn't. R finishes the computation, hands you a result, and mentions its reservations afterward. That result is sometimes fine and sometimes quietly wrong:
Both lines complete. The first recycles the shorter vector and warns longer object length is not a multiple of shorter object length - and recycling a length-2 vector against length-3 is almost never what anyone meant. The second warns NAs introduced by coercion and delivers a vector with a hole in it that will make every downstream mean() return NA. Treat both warnings as bugs to investigate, not noise to scroll past. In scripts you can enforce that stance with options(warn = 2), which promotes every warning to an error so nothing sneaks through.
Handling Failures With tryCatch()
Sometimes an error is expected - one corrupt file in a folder of hundreds, one bad row - and you want to handle it and move on rather than die. tryCatch() wraps a risky expression with handlers:
The mechanics: if the main block succeeds, its value is the result. If it errors, the error = handler runs instead and its return value (here NA) becomes the result - the script keeps going. conditionMessage(e) recovers the original message for logging. finally = runs in every case, success or failure, which is where cleanup like closing connections belongs - you can see it print before each result above.
There's a warning = handler too - tryCatch(as.numeric(x), warning = function(w) NA) catches the coercion warning from the previous section instead of letting it slide. One caution: a handler that returns a fallback without logging anything is a way of hiding failures, not handling them. Always record conditionMessage() - future you needs it.
Locating the Failure: traceback(), browser(), and Honest Printing
When the error comes from deep inside nested function calls, the message alone doesn't say which call chain got you there. Run traceback() immediately after the error:
f <- function(x) g(x)
g <- function(x) stop("boom")
f(1)
# Error in g(x) : boom
traceback()
# 2: g(x)
# 1: f(1)
It prints the call stack at the moment of failure - your call at one end, the failing call at the other. It must be the next thing you run; the stack is discarded once another error occurs.
For a live look, browser() pauses execution wherever you plant it and drops you into an interactive prompt inside the function - inspect variables, step with n, continue with c, quit with Q. debug(f) does the same without editing code: it flags f so its next call opens in the browser (undo with undebug(f)).
And then there's the technique nobody puts on conference slides but everyone uses: printing. Sprinkle print() or cat() at checkpoints, run, and see where reality stops matching your expectations. It's legitimate, it's fast, and in scripts it's often the most practical tool. Its best friend is str(), which answers the question behind maybe half of all R errors - "what actually is this object?":
One compact readout: it's a list, two elements, one an integer vector, one a data frame with these columns and types. When a $ fails or math misbehaves, str() the object before theorizing - the answer is usually right there ("...oh, it's a list of length 1 containing my data frame").
What You Take Away
- Read the message: the part after
Error insays where, the part after the colon says why. Fix the first error, not the loudest. object not found= typo, not-yet-run code, or a variable that only existed inside a function.could not find function= missinglibrary()call, almost always.unexpected symbolmeans unparseable grammar - and the real mistake is often an unclosed quote or paren before the flagged spot.- The type/index classics -
non-numeric argument,subscript out of bounds,$ on atomic vectors,argument is of length zero- each point at a wrong assumption about what an object is;str()checks the assumption in one call. - Warnings don't stop execution, which is exactly why they deserve attention - the result may be quietly wrong.
tryCatch(error =, warning =, finally =)handles expected failures without dying (always logconditionMessage());traceback()right after an error shows the call chain;browser()/debug()pause inside it;print()debugging is honest work.
Next up: many "errors" that aren't errors at all trace back to one three-character value - NA, and how missing values flow through everything R computes.
Frequently Asked Questions
What does "object 'x' not found" mean in R?
R looked for a variable named x and no such name exists in any environment it searched. The causes, in order of likelihood: a typo in the name (R is case-sensitive - Total is not total), the line that creates x hasn't been run yet in this session, or x was created inside a function and you're trying to use it outside.
What does "could not find function" mean in R?
The function exists in a package you haven't loaded this session. Installing a package is once per machine; library() is once per session, and forgetting the library() call is the usual cause. If library() itself fails, the package isn't installed. A typo in the function name produces the same error.
How do you handle errors in R with tryCatch?
Wrap the risky expression: tryCatch(expr, error = function(e) fallback, warning = function(w) fallback, finally = cleanup). If expr fails, the matching handler runs instead of the script dying, and whatever the handler returns becomes the result. conditionMessage(e) inside a handler gives you the original message for logging.
What does traceback() do in R?
Run immediately after an error, traceback() prints the chain of function calls that was active when the error fired - your call at one end, the failing call at the other. It doesn't fix anything; it tells you where to look, which is most of the battle when the error came from deep inside nested functions.