composerr

GitHub last commit GitHub code size in bytes

This R package makes it easy to add a full range and maintainable error handling to your functions and packages, without inflating your code.

Make error handling in R less tedious

Installation

# Install development version from GitHub
devtools::install_github('a-maldet/funky', build_opts = NULL)
devtools::install_github('a-maldet/composerr', build_opts = NULL)

Usage

library(composerr)

List of available functions

  • composerr(): Create new error handling function from scratch.
  • composerr(err_h = err_h_parent): Create new (child) error handling function from another (parent) error handler err_h_parent().
  • composerr_halt(err_h): Halt error processing for the error handler err_h and accumulate all error messages generated by calling err_h(msg, ...) in an internal error stack of err_h.
  • composerr_flush(err_h): Flush entire internal error stack of err_h, holding all stacked error messages generated by calling err_h(msg, ...).
  • composerr_counterr(err_h): Number of accumulated error messages generated by calling err_h().
  • composerr_get_action(err_h): Retrieve the ultimate error handler, which defines the ultimate processing of the full error message, when err_h(msg, ...) is called.
  • validate_composerr(err_h): Validate that err_h is indeed an error handler created by composerr().

Creating error handlers from scratch

The call

err_h1 <- composerr(before = "A problem appeared: ")

creates a composerr class object err_h1, which is basically a function err_h1 = function(msg, action = NULL, ...). Calling err_h1(msg) will throw an error with a meaningful error message:

err_h1("The fridge is empty.")
# Error: A problem appeared: The fridge is empty.

As one can see, the error message "The fridge is empty." is concatenated to the string "A problem appeared: ", which was defined in the creation of err_h1 with the argument before. If the argument collapse was set to NULL, then the total the resulting error message will be generated as follows:

msg_new <- paste0(before, msg, after)

If the argument collapse is not set to NULL, but is a string, then the strings before and after also will be concatenated, but afterwards the resulting character vector will be collapsed as well:

msg_new <- paste(paste0(before, msg, after), collapse = collapse)

Creating a child error handler

Sometimes it is useful, to create an error handler err_h_child from another error handler err_h_parent. In this case err_h_child is called a child error handler of err_h_parent and err_h_parent is called a parent error handler of err_h_child.

err_h_dinner <- composerr("Problem with dinner: ")
err_h_dinner_missing <- composerr(
  after = " is missing.",
  err_h = err_h_dinner
)

err_h_dinner_missing("Wine")
# Error: Problem with dinner: Wine is missing

err_h_dinner_missing("Food")
# Error: Problem with dinner: Food is missing

err_h_dinner("Guests are late.")
# Error: Problem with dinner: Guests are late.

Sometimes, one may even create an error handler from an error handler, which was itself is a child error handler of another error handler and so on. Let us assume that err_h10 is a child error handler of err_h9 and err_h9 if a child error handler of err_h8 and so on, down to err_h1. If you call err_h10(msg), then the resulting error message is created as follows:

msg_full <- paste0(before_h1, ..., before_h10, msg, after_h10, ..., after_h1)

Change behavior of error handler

If we have an error handler err_h1, then the final error message processing (also called ultimate error handling) has a large variety of possibilities:

  • throw an error with the created message text (stop(msg))
  • print a warning with the created message text (warning(msg))
  • print the created message text as normal text to the console (cat(msg))
  • write the created message text to a log file (cat(msg, file = FILE, append = TRUE, fill = TRUE))

The final error message processing mechanism of an error handler err_h1 can be defined by setting the ultimate error handler in the optional argument action. There are three possibilities how an ultimate error handler can be assigned to action:

  • action is directly passed to err_h1("MY ERROR TEXT", action = my_action)
  • action is not directly passed to err_h1(), but a default ultimate error handler was defined for err_h1 <- composerr("A problem appeared", action = my_default_action)
  • no action and no default action were defined: In this case stop is used as default action.

For example, let err_h1 be the following error handler

err_h1 <- composerr("There is a problem: ", action = warning)

err_h1("The fridge is empty.")
# Warning: There is a problem: The fridge is empty.

with a default ultimate error handler that will throw a warning when err_h1(msg) is called. In order to alter the behavior of err_h1 to printing a message instead of throwing a warning, one can call

err_h1("I am a message.", action = message)
# There is a problem: I am a message.

The action passed to the call err_h1() will ultimately be used for the processing of the error message, no matter which default ultimate error handlers where defined for err_h1 or the ancestor error handlers of err_h1. If the action argument is not passed to call err_h1(), then the default ultimate error handler defined for err_h1 (in this case action = warning) is used.

Setting the default ultimate error handler when creating an error handler

err_h <- composerr(action = my_default_action)

and optionally overwriting it by another ultimate error handler my_used_action when calling the error handler

err_h(msg, action = my_used_action)

allows a very flexible error handling mechanism.

The following example shows the implementation of a vector multiplication function with complete error handling:

my_vec_mult <- function(x, y) {
  # create your error handlers
  err_h <- composerr("In `my_vec_mult(): `")
  err_h_x <- composerr("Invalid argument `x`: ", err_h)
  err_h_x <- composerr("Invalid argument `y`: ", err_h)
  if (!is.numeric(x))
    err_h_x("Not a number.")
  if (any(is.na(x)))
    err_h_x("Has missings.", action = warning)
  if (!is.numeric(y))
    err_h_x("Not a number.")
  if (any(is.na(y)))
    err_h_x("Has missings.", action = warning)
  if (length(x) != length(y))
    err_h("Vectors `x` and `y` have different length.")
  sum(x*y, na.rm = TRUE)
}

my_vec_mult("a", 1:2)
# Error: In `my_vec_mult()`: Invalid argument `x`: Not a number.

my_vec_mult(c(1, NA), 1:2)
# Warning: In `my_vec_mult()`: Invalid argument `x`: Has missings.
# 1

my_vec_mult(1:2, "b")
# Error: In `my_vec_mult()`: Invalid argument `y`: Not a number.

my_vec_mult(1:2, c(1, NA))
# Warning: In `my_vec_mult()`: Invalid argument `y`: Has non-finite values.
# 1

my_vec_mult(1:2, 1:3)
# Error: In `my_vec_mult()`: Vectors `x` and `y` have different length.

my_vec_mult(1:2, 1:2)
# 14

In the next example, we create a function my_sum, which usually throws a warning if x contains missing values. The function also has an argument suppress_warnings that allows the warnings to be suppressed by optionally setting the default ultimate error handler to a function that does nothing:

my_sum <- function(x, suppress_warnings = FALSE) {
 if (isFALSE(suppress_warnings)) {
   # set default handler to throwing a warning
   err_h <- composerr("Problem in `my_sum()`: ", action = warning)
 } else {
   # set default handler to doing nothing
   err_h <- composerr(action = function(...) {})
 }
 if (any(is.na(x)))
   err_h("`x` has missing values.")
 sum(x, na.rm = TRUE)
}

my_sum(c(1, 2, NA))
# 3
# Warning: Problem in `my_sum()`: `x` has missing values.

my_sum(c(1, 2, NA), suppress_warnings = TRUE)
# 3

In the next example, we create an error handler err_h_with_log from another error handler err_h, which calls the same ultimate error handler as for err_h, but first writes the created error message to a log file:

logfile <- tempfile()
my_vecmult <- function(x, y) {
  err_h <- composerr("Error in call `my_vecmult()`")
  if (!is.numeric(x))
    err_h("Argument `x` is not numeric.")
  # forgot checking `y`
  computation_of_vecmult(x, y, err_h)
}
computation_of_vecmult <- function(x, y, err_h) {
  log_error <- function(msg)
    cat(
      paste0(msg, ": Used values: x = ", x, "; y = ", y),
      file = logfile,
      append = TRUE,
      fill = TRUE
    )
  action_without_log <- composerr_get_action(err_h)
  err_h_with_log <- composerr(
    err_h = err_h,
    action = function(msg, ...) {
      log_error(msg)
      action_without_log(msg, ...)
    }
  )
  tryCatch(
    sum(x*y),
    error = function(e) err_h_with_log(e)
  )
}
# Caught usage error (not logged)
my_vecmult("a", 1)
# Error: Error in call `my_vecmult(): Argument `x` is not numeric.`

# Caught internal error (logged)
my_vecmult(1, "a")
# Error: Error in call `my_vecmult()`: non-numeric argument for binary operator x * y

cat(paste(readLines(logfile), collapse = "\n"))
# Error in call `my_vecmult()`: non-numeric argument for binary operator x * y Used values: x = 1; y = a

Halt error processing and flush entire error stack at once

Sometimes it is useful to halt the error processing and just collect the error messages in an internal error stack and flush the entire stack later on. This can be done by calling:

  • composerr_halt(err_h): Halt the error processing
  • err_h(msg1), err_h(msg2), …: Not throwing errors any more, but collecting messages msg1, msg2, ... in an internal error stack.
  • composerr_flush(err_h): Flush the entire error stack at once

The following example shows a more advanced implementation of my_vec_mult2 by using a validation routine with a complete error handling:

validate_numeric_vec <- function(obj, err_h) {
  obj_name <- deparse(substitute(obj))
  err_h <- composerr(paste0("Invalid argument `", obj_name, "`"), err_h)
  if (!is.numeric(obj))
    err_h("Not a number.")
  err_h_list <- composerr(err_h = composerr("\n", err_h))
  composerr_halt(err_h_list)
  for (i in seq_along(obj)) {
    err_h_item <- composerr(before = paste0("  - Item-", i, " is "), err_h_list)
    if (is.na(obj[i]) && !is.nan(obj[i]))
      err_h_item("NA.")
    if (is.nan(obj[i]))
      err_h_item("NaN.")
    if (is.infinite(obj[i]))
      err_h_item("infinite.")
  }
  composerr_flush(err_h_list)
  invisible(obj)
}
my_vec_mult2 <- function(x, y) {
  err_h <- composerr("In `my_vec_mult2()`")
  validate_numeric_vec(x, err_h)
  validate_numeric_vec(y, err_h)
  if (length(x) != length(y))
    err_h("Vectors `x` and `y` have different length.")
  sum(x*y)
}

my_vec_mult2("a", 1:4)
# Error: In `my_vec_mult2()`: Invalid argument `x`: Not a number.

my_vec_mult2(c(1, NA, NaN, Inf, 5), 1:5)
# Error: In `my_vec_mult2()`: Invalid argument `x`:
#   - Item-2 is NA.
#   - Item-3 is NaN.
#   - Item-4 is infinite.

my_vec_mult2(1:5, c(NaN, 2, 3, NA, Inf))
# Error: In `my_vec_mult2()`: Invalid argument `y`:
#   - Item-1 is NA.
#   - Item-4 is NaN.
#   - Item-5 is infinite.

my_vec_mult2(1:5, 1:4)
# Error: In `my_vec_mult2()`: Vectors `x` and `y` have different length.

my_vec_mult2(1:5, 1:5)
# 55

Remark:

If you like this package, please give me a star on github: https://github.com/a-maldet/composerr

License

GPL-3