rguides

Error Handling in R: A Practical Guide

Error handling separates reliable R code from brittle scripts that crash on unexpected input. R provides several mechanisms to catch and recover from failures gracefully: tryCatch() for structured condition handling, try() for simple guard expressions, and custom condition classes for domain-specific error types. This guide covers all three.

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, and R skips every handler. Only when a condition is raised does R invoke the matching handler and skip the rest of the expression. This design means you pay zero runtime cost on the success path.

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, so more specific condition classes should come first. Placing a generic error = handler before a custom class handler would silently swallow the custom error before the specific handler ever runs.

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

Returning a sensible default on error is common in production code because callers don’t need to inspect the return value for failure markers. For code that needs simpler error checking without handler functions, the try() wrapper provides a lighter alternative.

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

Checking for failure later rather than handling it immediately is where try() shines. Running it inside lapply() collects results regardless of individual failures, allowing you to identify which inputs caused problems after the entire batch completes. This approach is particularly valuable in data pipelines where you process hundreds of files and want to know which ones failed without stopping the whole run:

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 accepts a silent parameter that suppresses the error message from appearing in the console. Setting silent = TRUE is useful in loops and batch processing where you don’t want each failure to print clutter, especially when you plan to inspect the try-error objects afterwards:

result <- try(10 / 0, silent = TRUE)

Raising your own errors and warnings

So far we have caught errors produced by R itself. In your own functions, you need to signal failures explicitly when preconditions are violated or when execution cannot proceed safely. Use stop() to raise an error and halt execution, and warning() to flag a problem while continuing:

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, the pattern is similar: call warning() inside your function to signal a non-fatal issue. Unlike errors, warnings do not halt execution; the function continues running and returns a result. This makes warnings appropriate for deprecation notices and suspicious-but-recoverable conditions:

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 that extend the base error, warning, or condition types, letting callers distinguish between different failure modes and handle each one differently. This matters when a library function can fail for several distinct reasons and the caller needs to respond correctly to each:

# 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, just as with built-in condition types. Custom classes make your error handling precise rather than coarse-grained.

Finally: cleanup code

Sometimes you need to run code regardless of whether an error occurred — closing file connections, releasing database locks, or removing temporary files. The finally argument of tryCatch() executes after the expression and any handler, guaranteeing cleanup on both success and failure paths:

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:

  1. 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.

  2. Warnings don’t stop execution by default. They print a message but the code continues. Use options(warn = 2) to turn warnings into errors.

  3. The condition object has useful fields. Access them with $: e$message, e$call, e$callingFunc.

  4. 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.

Choosing the right tool

R gives you three main error handling tools, and each serves a different purpose. Use tryCatch() when you need structured handling with separate code paths for different conditions. Its handler functions let you inspect the error, log it, return a fallback value, or even re-signal it after cleanup. The finally block guarantees resource cleanup regardless of outcome.

Use try() when you want to defer error checking. It is less verbose than tryCatch() and fits naturally in loops and lapply() calls where you need to collect both successes and failures without breaking the iteration. The tradeoff is that try() treats all errors the same, so you lose the ability to distinguish between different failure modes.

For package authors, withCallingHandlers() fills a niche that neither tryCatch() nor try() covers. It handles conditions without unwinding the call stack, which means you can log a warning or increment a counter and then let the condition propagate normally. Use it for monitoring and auditing rather than recovery, when you want to observe failures without intercepting them.

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 finally for cleanup code
  • Consider custom condition classes for complex applications

For production code, these patterns keep your programs running even when unexpected things happen.

See also