rguides

Error Handling in R, Learn how to handle errors and exceptions

Error handling is a critical skill for writing reliable R code. Whether you are reading files that might not exist, calling APIs that might fail, or working with user input, your code needs to handle unexpected situations gracefully. In this tutorial, you will learn the fundamentals of error handling in R.

What you’ll learn

This tutorial covers the key concepts and practical techniques for working with Error Handling in R, Learn how to handle errors and exceptions. By the end, you will know how to apply the core functions in real data analysis workflows.

Understanding errors vs warnings

R distinguishes between errors and warnings. An error stops execution immediately, while a warning signals a potential problem but allows execution to continue.

# This generates a warning but continues
log(-1)
# [1] NaN
# Warning message:
# In log(-1) : NaNs produced

# This would generate an error in a function
divide <- function(a, b) {
  if (b == 0) stop("Cannot divide by zero!")
  a / b
}

divide(10, 0)
# Error in divide(10, 0) : Cannot divide by zero!

The tryCatch function

The primary mechanism for error handling in R is tryCatch(). It lets you catch errors and execute alternative code:

result <- tryCatch(
  {
    # Code to try
    dangerous_function()
  },
  error = function(e) {
    # Code to run if error occurs
    message("An error occurred: ", e$message)
    NA  # Return NA on error
  },
  warning = function(w) {
    # Code to run if warning occurs
    message("Warning: ", w$message)
    NA  # Return NA on warning
  },
  finally = {
    # Code that always runs (cleanup)
    message("Execution complete")
  }
)

Practical example: reading files safely

A common use case is reading files that might not exist:

read_csv_safe <- function(filepath) {
  tryCatch(
    {
      read.csv(filepath)
      message("Successfully read: ", filepath)
    },
    error = function(e) {
      message("Failed to read ", filepath, ": ", e$message)
      NULL  # Return NULL instead of failing
    }
  )
}

# Usage
data <- read_csv_safe("data.csv")
data <- read_csv_safe("nonexistent.csv")
# Failed to read nonexistent.csv: cannot open the connection

Handling multiple error types

You can handle different error conditions with separate condition handlers:

safe_divide <- function(a, b) {
  tryCatch(
    {
      if (!is.numeric(a) || !is.numeric(b)) {
        stop("Arguments must be numeric")
      }
      if (b == 0) {
        stop("Division by zero")
      }
      a / b
    },
    error = function(e) {
      message("Error: ", e$message)
      NA
    },
    warning = function(w) {
      message("Warning: ", w$message)
      NA
    }
  )
}

safe_divide(10, 2)
# [1] 5

safe_divide(10, 0)
# Error: Division by zero
# [1] NA

safe_divide("a", 2)
# Error: Arguments must be numeric
# [1] NA

The try function for inline handling

For simpler cases, try() wraps an expression and returns an object with status information:

# Using try() for simple error handling
result <- try(dangerous_operation(), silent = TRUE)

if (inherits(result, "try-error")) {
  message("Operation failed: ", attr(result, "condition")$message)
} else {
  # Use result
}

Using purrr for functional error handling

The tidyverse approach uses purrr’s safely(), possibly(), and quietly() functions:

library(purrr)

# safely() returns a list with result and error
safe_log <- safely(log)

safe_log(10)
# $result
# [1] 2.302585
# $error
# NULL

safe_log(-10)
# $result
# NULL
# $error
# <simpleError in log(-10): NaNs produced>

# possibly() provides a default value for errors
positive_log <- possibly(log, otherwise = NA_real_)

positive_log(10)
# [1] 2.302585

positive_log(-10)
# [1] NA

Creating custom error classes

For more complex applications, create custom error classes:

# Define custom error class
validate_positive <- function(x, name = "value") {
  if (!is.numeric(x) || x <= 0) {
    stop(simpleError(
      paste(name, "must be a positive number, got:", x),
      call = sys.call()
    ))
  }
  x
}

# Using custom validation
divide_safe <- function(a, b) {
  tryCatch(
    {
      validate_positive(a, "a")
      validate_positive(b, "b")
      a / b
    },
    error = function(e) {
      message("Validation failed: ", e$message)
      NA
    }
  )
}

divide_safe(10, -5)
# Validation failed: b must be a positive number, got: -5
# [1] NA

Best practices

  1. Catch specific errors - Handle specific error conditions rather than catching everything
  2. Provide meaningful error messages - Help users understand what went wrong
  3. Clean up with finally - Release resources (close connections, files) in the finally block
  4. Do not suppress errors silently - At minimum, log or report errors
  5. Test error handling - Use testthat or similar to verify your error paths work

Understanding conditions

R uses a hierarchical condition system: errors, warnings, and messages are all conditions. An error stops execution; a warning signals a potential problem but continues; a message provides information. All three can be caught with tryCatch(). The distinction matters: tryCatch(log(-1)) catches the warning from log() of a negative number, which produces NaN with a warning rather than an error.

tryCatch in practice

result <- tryCatch({
  # code that might fail
  readLines("missing_file.txt")
}, error = function(e) {
  message("File not found: ", conditionMessage(e))
  NULL  # return NULL on error
}, warning = function(w) {
  message("Warning: ", conditionMessage(w))
  NULL
})

The handler function receives the condition object. conditionMessage(e) extracts the message. Return a default value from the handler to continue execution.

withCallingHandlers

Unlike tryCatch(), withCallingHandlers() handles the condition without unwinding the stack. This means execution resumes after the signal. Use it for logging warnings without interrupting the computation:

withCallingHandlers({
  result <- complex_computation()
}, warning = function(w) {
  log_warning(conditionMessage(w))
  invokeRestart("muffleWarning")  # suppress the warning display
})

invokeRestart("muffleWarning") prevents the warning from propagating further after handling.

Custom conditions

rlang::abort() creates structured conditions with a class: abort("invalid input", class = "validation_error", field = "age"). Callers can catch by class: tryCatch(expr, validation_error = function(e) ...). This is more precise than catching all errors and checking the message string. Package authors should use classed conditions for expected failure modes so callers can handle them specifically.

Common error patterns

Understanding common errors accelerates debugging. object 'x' not found means x has not been assigned or was removed. could not find function "fn" means the package providing fn is not loaded. non-numeric argument to binary operator usually means NA was introduced by type conversion. argument implies differing number of rows in data.frame() means column lengths differ. Reading error messages carefully and checking the call stack with traceback() resolves most issues.

Signaling your own conditions

Package functions should signal informative conditions rather than raw errors. rlang::abort("x must be positive", .data = list(x = x)) creates a classed error with attached data. Users can access the data with rlang::cnd_data(e) in the handler. rlang::warn() creates a warning; rlang::inform() creates a message. Classed conditions let callers catch specific error types without parsing error strings.

The condition system

R’s condition system is more general than try/catch in most languages. Conditions are S3 objects with a class hierarchy. Errors (stop()), warnings (warning()), and messages (message()) are all conditions. Custom condition classes extend this hierarchy.

When a condition is signaled, R searches up the call stack for handlers. If no handler is found for an error, execution stops and the error message is printed. tryCatch() installs handlers that run when conditions of specified classes are signaled.

simpleError("text"), simpleWarning("text"), and simpleMessage("text") create the simplest condition objects. structure(list(message = "text", call = sys.call()), class = c("my_error", "error", "condition")) creates a custom error class.

withCallingHandlers vs tryCatch

tryCatch() unwinds the call stack when a condition is handled, execution continues after the tryCatch() call, not from where the condition was signaled. This is appropriate for recovery: catch an error, return a fallback value, and continue.

withCallingHandlers() handles conditions without unwinding. After the handler runs, execution resumes from where the condition was signaled (unless the handler calls invokeRestart() or the condition is re-signaled). This is appropriate for logging — log a warning without suppressing it.

withCallingHandlers(
  risky_function(),
  warning = function(w) {
    log_warning(conditionMessage(w))
    invokeRestart("muffleWarning")  # suppress the warning after logging
  }
)

invokeRestart("muffleWarning") prevents the warning from propagating further. Without it, the warning would continue up the stack after the handler returns.

purrr::safely and purrr::possibly

For mapping over collections where some operations may fail, purrr::safely() wraps a function to capture results and errors separately: safely(log)(c(-1, 1, 2)) returns a list where each element is list(result = ..., error = ...). Errors become NULL results; successful results have NULL errors.

purrr::possibly(fn, otherwise = NA) is simpler: it returns otherwise on error instead of stopping. map_dbl(values, possibly(risky_fn, NA_real_)) maps with error replacement, giving a numeric vector with NA for failures.

purrr::quietly(fn) captures messages, warnings, and output in addition to the result, returning all four in a list. Useful for functions that print to stdout during execution.

Custom conditions and restarts

Well-designed package code signals informative custom conditions rather than plain error strings. Custom conditions carry structured data that handlers can access programmatically:

abort_data_not_found <- function(id) {
  rlang::abort(
    message = paste("ID not found:", id),
    class = "data_not_found",
    id = id
  )
}

rlang::abort() creates conditions with the rlang_error class and stores extra data. Handlers can access it: tryCatch(fn(), data_not_found = function(e) handle_missing(e$id)).

Restarts allow signaling code to offer multiple recovery options. withRestarts(risky_operation(), use_default = function() default_value) offers a restart. Handlers invoke it: invokeRestart("use_default"). This decouples signaling from handling — the function that signals does not need to know how calling code wants to handle the condition.

Defensive programming patterns

stopifnot(is.numeric(x), length(x) > 0) checks preconditions. On failure, it generates an error message naming the failed condition. rlang::arg_match(method, c("pearson", "spearman", "kendall")) validates function arguments against allowed values, with a helpful error showing the invalid value and valid options.

For user-facing packages, provide specific, actionable error messages. “Expected a numeric vector but got a character vector in column ‘age’” is better than “non-numeric argument to mathematical function”. cli::cli_abort("Expected {.cls numeric}, not {.cls {class(x)[1]}}") using cli markup produces consistently formatted errors.

Why error handling matters in automation

Interactive R sessions tolerate errors gracefully — you see the error message, read it, and try again. Automated scripts and production pipelines have no human to read the error and respond. Unhandled errors crash the script at the point of failure, potentially leaving downstream processes in an inconsistent state. Proper error handling lets automation continue after recoverable failures, log failures for later review, and fail gracefully for unrecoverable ones.

Error handling is also important for package functions. A function that crashes with an obscure internal error message when given invalid input is harder to use than one that checks its inputs and raises a clear error explaining what was wrong and how to fix it. The distinction between user error (wrong input) and internal error (bug in the function) should be clear from the error message.

The condition hierarchy

R’s condition system is hierarchical. Conditions are messages, warnings, or errors (or custom types). Errors halt execution unless caught. Warnings continue execution but signal something unexpected. Messages are informational. The hierarchy means handlers for errors do not catch warnings, and handlers for warnings do not catch messages. This specificity allows you to handle only the conditions you expect without accidentally swallowing unexpected ones.

Calling tryCatch with specific condition classes — not just the catch-all “error” class — provides finer control. If a function might raise either a network error or a parse error, you can handle them differently: retry the network error, skip the record on parse error, and propagate anything else. The conditionMessage function extracts the message from a caught condition, which is useful for logging or re-raising with additional context.

Building pipelines that tolerate failures

For processing many items where some will fail, purrr’s safely and possibly transforms a function that might error into one that always returns a result. safely returns a list with result and error components. possibly returns a default value on error. Both allow map to run to completion over a list of inputs even when some inputs cause errors.

After processing, inspecting which items failed helps diagnose systematic problems. Filtering the result list to the error components and examining the error messages identifies whether failures are random or correlated with specific input characteristics. A failure rate of 5% in a large batch might be acceptable if the failures are random; a failure rate of 100% for items from a specific source indicates a systematic problem to investigate.

Summary

Error handling in R uses tryCatch() as the primary mechanism, with try() for simpler cases and purrr’s safely() and possibly() for functional programming contexts. Always provide meaningful error messages, clean up resources, and test your error handling paths.

In the next tutorial, you will apply these concepts as you build more complex R programs that gracefully handle unexpected inputs and conditions.

Next steps

Now that you understand error handling in r — learn how to handle errors and exceptions, explore these related topics to deepen your knowledge and apply these techniques in more complex scenarios.