R - tryCatch() Error Handling
The `tryCatch()` function wraps code that might fail and defines handlers for different conditions. The basic syntax includes an expression to evaluate and named handler functions.
Key Insights
- tryCatch() provides structured error handling in R through a four-part mechanism: expression evaluation, error handlers, warning handlers, and finally clauses for cleanup operations
- Custom error classes and condition handling enable sophisticated error recovery strategies, including retry logic, fallback values, and graceful degradation in production systems
- Proper error handling separates exceptional conditions from normal control flow, making R code more maintainable and preventing silent failures in data pipelines
Basic tryCatch() Structure
The tryCatch() function wraps code that might fail and defines handlers for different conditions. The basic syntax includes an expression to evaluate and named handler functions.
result <- tryCatch(
expr = {
# Code that might fail
log(-1)
},
error = function(e) {
# Handle errors
message("Error caught: ", e$message)
return(NA)
},
warning = function(w) {
# Handle warnings
message("Warning caught: ", w$message)
return(NULL)
},
finally = {
# Always executes (cleanup)
message("Execution completed")
}
)
The expression executes first. If an error occurs, control transfers to the error handler. The finally block always executes regardless of success or failure, making it ideal for cleanup operations like closing file connections or database handles.
Error Handler Implementation
Error handlers receive a condition object containing the error message and call stack. You can extract information and implement recovery logic.
safe_divide <- function(x, y) {
tryCatch(
expr = {
if (y == 0) {
stop("Division by zero attempted")
}
x / y
},
error = function(e) {
cat("Error in safe_divide:", e$message, "\n")
cat("Call:", deparse(e$call), "\n")
return(Inf)
}
)
}
# Usage
safe_divide(10, 2) # Returns 5
safe_divide(10, 0) # Returns Inf with error message
For production code, logging errors with context information helps debugging:
process_data <- function(data_file) {
tryCatch(
expr = {
data <- read.csv(data_file)
# Processing logic
return(data)
},
error = function(e) {
log_entry <- list(
timestamp = Sys.time(),
file = data_file,
error = e$message,
traceback = sys.calls()
)
saveRDS(log_entry, paste0("error_", format(Sys.time(), "%Y%m%d_%H%M%S"), ".rds"))
return(NULL)
}
)
}
Warning Handling Strategies
Warnings indicate potential problems without stopping execution. The warning handler can suppress, log, or convert warnings to errors.
strict_sqrt <- function(x) {
tryCatch(
expr = {
result <- sqrt(x)
return(result)
},
warning = function(w) {
# Convert warning to error
stop("Converted warning: ", w$message)
}
)
}
# This will throw an error instead of warning
strict_sqrt(-4)
# Logging warnings without stopping
logged_sqrt <- function(x) {
warnings_list <- list()
result <- tryCatch(
expr = sqrt(x),
warning = function(w) {
warnings_list <<- c(warnings_list, list(w$message))
invokeRestart("muffleWarning")
return(NaN)
}
)
if (length(warnings_list) > 0) {
attr(result, "warnings") <- warnings_list
}
return(result)
}
Custom Condition Classes
Creating custom condition classes enables precise error handling for different failure modes in complex applications.
# Define custom conditions
validation_error <- function(message, field) {
structure(
list(message = message, field = field),
class = c("validation_error", "error", "condition")
)
}
connection_error <- function(message, host) {
structure(
list(message = message, host = host),
class = c("connection_error", "error", "condition")
)
}
# Function using custom errors
validate_and_connect <- function(host, port) {
tryCatch(
expr = {
if (port < 1 || port > 65535) {
stop(validation_error("Invalid port number", "port"))
}
# Simulate connection attempt
if (host == "unreachable.example.com") {
stop(connection_error("Host unreachable", host))
}
return(list(status = "connected", host = host, port = port))
},
validation_error = function(e) {
message("Validation failed for field: ", e$field)
return(list(status = "validation_failed", field = e$field))
},
connection_error = function(e) {
message("Cannot connect to: ", e$host)
return(list(status = "connection_failed", host = e$host))
},
error = function(e) {
message("Unexpected error: ", e$message)
return(list(status = "unknown_error"))
}
)
}
# Usage
validate_and_connect("example.com", 8080)
validate_and_connect("example.com", 99999)
validate_and_connect("unreachable.example.com", 8080)
Retry Logic with Exponential Backoff
Implementing retry logic for transient failures improves reliability in network operations and external service calls.
retry_with_backoff <- function(expr, max_attempts = 3, base_delay = 1) {
attempt <- 1
while (attempt <= max_attempts) {
result <- tryCatch(
expr = {
eval(expr)
},
error = function(e) {
if (attempt == max_attempts) {
stop("Max retry attempts reached. Last error: ", e$message)
}
delay <- base_delay * (2 ^ (attempt - 1))
message(sprintf("Attempt %d failed: %s. Retrying in %d seconds...",
attempt, e$message, delay))
Sys.sleep(delay)
return(NULL)
}
)
if (!is.null(result)) {
return(result)
}
attempt <- attempt + 1
}
}
# Simulate unreliable API call
unreliable_api <- function() {
if (runif(1) < 0.7) {
stop("API temporarily unavailable")
}
return(list(status = "success", data = rnorm(10)))
}
# Use retry logic
result <- retry_with_backoff(quote(unreliable_api()), max_attempts = 5, base_delay = 1)
Resource Management with finally
The finally clause ensures cleanup code executes regardless of success or failure, critical for managing files, database connections, and temporary resources.
process_large_file <- function(input_path, output_path) {
input_conn <- NULL
output_conn <- NULL
temp_file <- NULL
tryCatch(
expr = {
# Open connections
input_conn <- file(input_path, "r")
output_conn <- file(output_path, "w")
temp_file <- tempfile()
# Process data
lines <- readLines(input_conn)
processed <- toupper(lines)
writeLines(processed, output_conn)
return(length(processed))
},
error = function(e) {
message("Processing failed: ", e$message)
return(-1)
},
finally = {
# Guaranteed cleanup
if (!is.null(input_conn) && isOpen(input_conn)) {
close(input_conn)
message("Input connection closed")
}
if (!is.null(output_conn) && isOpen(output_conn)) {
close(output_conn)
message("Output connection closed")
}
if (!is.null(temp_file) && file.exists(temp_file)) {
unlink(temp_file)
message("Temporary file removed")
}
}
)
}
Nested tryCatch for Complex Workflows
Complex data pipelines require multiple error handling layers with different recovery strategies at each level.
data_pipeline <- function(source_file) {
tryCatch(
expr = {
# Stage 1: Load data
raw_data <- tryCatch(
expr = read.csv(source_file),
error = function(e) {
message("Failed to load data, trying backup source")
read.csv(paste0(source_file, ".backup"))
}
)
# Stage 2: Validate data
validated_data <- tryCatch(
expr = {
if (nrow(raw_data) == 0) stop("Empty dataset")
if (!all(c("id", "value") %in% names(raw_data))) {
stop("Missing required columns")
}
raw_data
},
error = function(e) {
message("Validation failed: ", e$message)
# Return minimal valid structure
data.frame(id = integer(), value = numeric())
}
)
# Stage 3: Transform data
result <- tryCatch(
expr = {
validated_data$value <- log(validated_data$value)
validated_data
},
warning = function(w) {
message("Transformation warning: ", w$message)
validated_data$value <- ifelse(validated_data$value <= 0,
NA,
log(validated_data$value))
return(validated_data)
}
)
return(result)
},
error = function(e) {
message("Pipeline failed catastrophically: ", e$message)
return(NULL)
}
)
}
This layered approach provides granular control over error recovery while maintaining a fallback for unexpected failures. Each stage handles its own errors appropriately, with outer handlers catching only unrecoverable conditions.