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.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.