Error Handling in R
Errors happen in every programming language. R provides several mechanisms to handle them gracefully. This guide covers the main approaches: tryCatch(), try(), and custom condition classes.
The Problem with Unhandled Errors
When R encounters an error, it stops execution immediately. In interactive use, this is fine. In production code—scripts, packages, Shiny apps—unhandled errors crash your program.
Consider a function that divides numbers:
safe_divide <- function(a, b) {
a / b
}
safe_divide(10, 2)
# [1] 5
safe_divide(10, 0)
# Error in a/b : non-numeric argument to binary operator
The second call stops execution. That’s not ideal when you want your code to continue running.
Catching Errors with tryCatch()
The tryCatch() function is R’s main mechanism for handling conditions. It has two parts: the code to try, and handlers for different conditions.
result <- tryCatch({
# Code that might fail
10 / 0
}, error = function(e) {
# What to do when an error occurs
message("An error happened: ", e$message)
NA
})
# An error happened: non-numeric argument to binary operator
result
# [1] NA
The key insight: the code inside tryCatch() runs normally if it succeeds. Only when a condition is raised does the handler run.
Handling Multiple Conditions
You can handle errors, warnings, and other conditions separately:
result <- tryCatch({
# This generates a warning
log(-1)
}, warning = function(w) {
"Warning was signaled"
}, error = function(e) {
"Error was signaled"
})
result
# [1] "Warning was signaled"
Order matters when you have multiple handlers. R checks them in order and uses the first matching handler. More specific conditions should come first.
Returning Values from tryCatch()
The handler functions return values that become the result of tryCatch():
divide_safely <- function(a, b) {
tryCatch({
if (b == 0) {
stop("Cannot divide by zero")
}
a / b
}, error = function(e) {
NA_real_
})
}
divide_safely(10, 2)
# [1] 5
divide_safely(10, 0)
# [1] NA
This pattern—returning a sensible default on error—is common in production code.
Using try() for Simpler Cases
The try() function is a simpler wrapper around tryCatch(). It catches errors and returns an object of class “try-error” if something fails:
result <- try(10 / 0)
class(result)
# [1] "try-error"
# Check if it failed
if (inherits(result, "try-error")) {
message("Operation failed")
}
# Operation failed
This is useful when you want to check for failure later, rather than handling it immediately:
results <- lapply(1:5, function(i) {
try(log(i))
})
# Find which ones failed
failed <- sapply(results, inherits, "try-error")
which(failed)
# integer(0) -- none failed in this case
The try() function also accepts a silent parameter to suppress error messages:
result <- try(10 / 0, silent = TRUE)
Raising Your Own Errors and Warnings
Sometimes you need to signal that something went wrong in your own code. Use stop() for errors and warning() for warnings:
check_positive <- function(x) {
if (any(x < 0)) {
stop("All values must be positive")
}
x
}
check_positive(c(1, 2, 3))
# [1] 1 2 3
check_positive(c(1, -2, 3))
# Error in check_positive(c(1, -2, 3)) : All values must be positive
For warnings:
deprecated_function <- function(x) {
warning("This function is deprecated, use new_function() instead")
x * 2
}
deprecated_function(5)
# Warning message:
# In deprecated_function(5) :
# This function is deprecated, use new_function() instead
# [1] 10
Custom Condition Classes
R’s condition system is object-oriented. You can create custom condition classes to distinguish between different types of errors:
# Define custom error class
invalid_input_error <- function(message) {
err <- structure(
list(message = message, call = sys.call(-1)),
class = c("invalid_input_error", "error", "condition")
)
signalCondition(err)
}
# Handle it specifically
handle_input <- function(x) {
tryCatch({
if (!is.numeric(x)) {
invalid_input_error("Input must be numeric")
}
x + 1
}, invalid_input_error = function(e) {
message("Invalid input: ", e$message)
NA
}, error = function(e) {
message("Unexpected error: ", e$message)
NA
})
}
handle_input("hello")
# Invalid input: Input must be numeric
# [1] NA
The handler order matters here too. Put specific handlers before general ones.
Finally: Cleanup Code
Sometimes you need to run code regardless of whether an error occurred—closing file connections, releasing resources. Use the finally argument:
read_and_process <- function(filepath) {
con <- file(filepath, "r")
tryCatch({
readLines(con, n = 1)
}, error = function(e) {
message("Error reading file: ", e$message)
NULL
}, finally = {
close(con)
message("File connection closed")
})
}
The finally block runs whether the code succeeds or fails.
Common Patterns and Gotchas
A few things trip up newcomers to R error handling:
-
tryCatch returns the handler’s result, not the original code’s result. If your code succeeds but you have an error handler that returns something, you get the handler’s return value.
-
Warnings don’t stop execution by default. They print a message but the code continues. Use
options(warn = 2)to turn warnings into errors. -
The condition object has useful fields. Access them with
$:e$message,e$call,e$callingFunc. -
Non-local jumps unwind the stack. This means
tryCatch()catches errors from functions called deep in the call stack, but it also means the call stack is lost.
Conclusion
Error handling in R centers on tryCatch(). Master this function and you can handle errors, warnings, and custom conditions gracefully. The key patterns are:
- Return sensible defaults on error
- Handle specific conditions before general ones
- Use
finallyfor cleanup code - Consider custom condition classes for complex applications
For production code, these patterns keep your programs running even when unexpected things happen.