8.5 Custom conditions

8.5.1 Motivation

The base::log() function provides a minimal error message.

log(letters)
#> Error in log(letters): non-numeric argument to mathematical function
log(1:10, base = letters)
#> Error in log(1:10, base = letters): non-numeric argument to mathematical function

One could make a more informative error message about which argument is problematic.

my_log <- function(x, base = exp(1)) {
  if (!is.numeric(x)) {
    rlang::abort(paste0(
      "`x` must be a numeric vector; not ", typeof(x), "."
    ))
  }
  if (!is.numeric(base)) {
    rlang::abort(paste0(
      "`base` must be a numeric vector; not ", typeof(base), "."
    ))
  }

  base::log(x, base = base)
}

Consider the difference:

my_log(letters)
#> Error in `my_log()`:
#> ! `x` must be a numeric vector; not character.
my_log(1:10, base = letters)
#> Error in `my_log()`:
#> ! `base` must be a numeric vector; not character.

8.5.2 Signalling

Create a helper function to describe errors:

abort_bad_argument <- function(arg, must, not = NULL) {
  msg <- glue::glue("`{arg}` must {must}")
  if (!is.null(not)) {
    not <- typeof(not)
    msg <- glue::glue("{msg}; not {not}.")
  }
  
  rlang::abort(
    "error_bad_argument", # <- this is the (error) class, I believe
    message = msg, 
    arg = arg, 
    must = must, 
    not = not
  )
}

Rewrite the log function to use this helper function:

my_log <- function(x, base = exp(1)) {
  if (!is.numeric(x)) {
    abort_bad_argument("x", must = "be numeric", not = x)
  }
  if (!is.numeric(base)) {
    abort_bad_argument("base", must = "be numeric", not = base)
  }

  base::log(x, base = base)
}

See the result for the end user:

my_log(letters)
#> Error in `abort_bad_argument()`:
#> ! `x` must be numeric; not character.
my_log(1:10, base = letters)
#> Error in `abort_bad_argument()`:
#> ! `base` must be numeric; not character.

8.5.3 Handling

Use class of condition object to allow for different handling of different types of errors

tryCatch(
  error_bad_argument = function(cnd) "bad_argument",
  error = function(cnd) "other error",
  my_log("a")
)
#> [1] "bad_argument"

But note that the first handler that matches any of the signal’s class, potentially in a vector of signal classes, will get control. So put the most specific handlers first.