In this lab, we will learn how to handle conditions.

Goal: by the end of this lab, you should be able to handle an error and return gracefully.

Three types of conditions

Recall that there are three main types of conditions in R:

  • error: will terminate the function immediately and throw an error
  • warning: will continue to execute the function, but will warn you about what happened afterwards
  • message: will immediately display a message about what is happening.

The following function will compute the number of unique values in whatever columns in a data frame you select. The first argument is the data frame, and the second argument is the dots, which are simply passed to select().

Note that this function may display a message, or a warning(), or an error, depending on what happens.

unique_values <- function(.data, ...) {
  x <- .data %>%
    select(...)
  
  if (ncol(x) == ncol(.data)) {
    warning("All columns selected!")
  }
  
  if (ncol(x) < 1) {
    stop("No columns selected.")
  }

  message(paste("Computing on", ncol(x), "columns..."))  
  map_int(x, n_distinct)
}
  1. Examine the code for unique_values() carefully. Do you understand how it works?

If we give this function no valid selection, we will throw a warning.

unique_values(starwars)
## Error in unique_values(starwars): No columns selected.
  1. Was the error in the previous example thrown by unique_values() or by select()? How do you know?

If we select all of the columns in the data frame, then we get a warning. However, the code still executes.

unique_values(starwars, everything())
## Warning in unique_values(starwars, everything()): All columns selected!
## Computing on 14 columns...
##       name     height       mass hair_color skin_color  eye_color birth_year 
##         87         46         39         13         31         15         37 
##        sex     gender  homeworld    species      films   vehicles  starships 
##          5          3         49         38         24         11         17
  1. Why does the warning message show up after the results? Why does the warning message show up after the message, when the line of code with message() came after the call to warning() in the function?

Since unique_values() passes the dots to select(), we can leverage all of the functionality of the select helpers!

unique_values(starwars, contains("n"))
## Computing on 3 columns...
##       name skin_color     gender 
##         87         31          3

However, if we pass garbage to select(), then of course select() will still throw an error.

unique_values(starwars, i_love_r)
## Error in `select()`:
## ! Can't subset columns that don't exist.
## ✖ Column `i_love_r` doesn't exist.
  1. Consider the difference in the output between the previous example and unique_values(starwars). What is different?

Catching errors

Instead of just failing whenever the user passes bad arguments to select(), we might want to catch those errors and do something with them. Here, we use a tryCatch() statement to provide some additional information about what went wrong, and to continue with the original data frame if the select() statement failed.

unique_values_safe <- function(.data, ...) {
  x <- tryCatch(
    error = function(cnd) {
      warning("Attempt to select column has failed")
      message("Here is what we know about the error")
      str(cnd)
      .data
    },
    .data %>%
      select(...)
  )
  
  if (ncol(x) == ncol(.data)) {
    warning("All columns selected!")
  }
  
  if (ncol(x) < 1) {
    stop("No columns selected.")
  }

  message(paste("Computing on", ncol(x), "columns..."))  
  map_int(x, n_distinct)
}

Now, even though an error still occurs, we still get output.

unique_values_safe(starwars, i_love_r)
## Warning in value[[3L]](cond): Attempt to select column has failed
## Here is what we know about the error
## List of 9
##  $ message         : chr ""
##  $ trace           :Classes 'rlang_trace', 'rlib_trace', 'tbl' and 'data.frame': 33 obs. of  6 variables:
##   ..$ call       :List of 33
##   .. ..$ : language unique_values_safe(starwars, i_love_r)
##   .. ..$ : language tryCatch(error = function(cnd) {     warning("Attempt to select column has failed") ...
##   .. .. ..- attr(*, "srcref")= 'srcref' int [1:8] 2 3 11 3 3 3 2 11
##   .. .. .. ..- attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x7fe571fb6310> 
##   .. ..$ : language tryCatchList(expr, classes, parentenv, handlers)
##   .. ..$ : language tryCatchOne(expr, names, parentenv, handlers[[1L]])
##   .. ..$ : language doTryCatch(return(expr), name, parentenv, handler)
##   .. ..$ : language .data %>% select(...)
##   .. ..$ : language select(., ...)
##   .. ..$ : language select.data.frame(., ...)
##   .. ..$ : language tidyselect_fix_call(tidyselect::eval_select(expr(c(...)), .data), call = error_call)
##   .. ..$ : language withCallingHandlers(expr, error = function(cnd) {     cnd$call <- call ...
##   .. ..$ : language tidyselect::eval_select(expr(c(...)), .data)
##   .. ..$ : language eval_select_impl(data, names(data), as_quosure(expr, env), include = include,      exclude = exclude, strict = st| __truncated__ ...
##   .. ..$ : language with_subscript_errors(vars_select_eval(vars, expr, strict = strict, data = x,      name_spec = name_spec, uniquel| __truncated__ ...
##   .. ..$ : language tryCatch(with_entraced_errors(expr), vctrs_error_subscript = function(cnd) {     cnd$subscript_action <- subscript_action(type) ...
##   .. ..$ : language tryCatchList(expr, classes, parentenv, handlers)
##   .. ..$ : language tryCatchOne(expr, names, parentenv, handlers[[1L]])
##   .. ..$ : language doTryCatch(return(expr), name, parentenv, handler)
##   .. ..$ : language with_entraced_errors(expr)
##   .. ..$ : language try_fetch(expr, simpleError = function(cnd) {     abort(conditionMessage(cnd), call = conditionCall(cnd)) ...
##   .. ..$ : language withCallingHandlers(expr, simpleError = function(cnd) {     { ...
##   .. ..$ : language vars_select_eval(vars, expr, strict = strict, data = x, name_spec = name_spec,      uniquely_named = uniquely_nam| __truncated__ ...
##   .. ..$ : language walk_data_tree(expr, data_mask, context_mask, error_call)
##   .. ..$ : language eval_c(expr, data_mask, context_mask)
##   .. ..$ : language reduce_sels(node, data_mask, context_mask, init = init)
##   .. ..$ : language walk_data_tree(new, data_mask, context_mask)
##   .. ..$ : language as_indices_sel_impl(out, vars = vars, strict = strict, data = data, call = error_call)
##   .. ..$ : language as_indices_impl(x, vars, call = call, strict = strict)
##   .. ..$ : language chr_as_locations(x, vars, call = call)
##   .. ..$ : language vctrs::vec_as_location(x, n = length(vars), names = vars)
##   .. ..$ : language `<fn>`()
##   .. ..$ : language stop_subscript_oob(i = i, subscript_type = subscript_type, names = names,      subscript_action = subscript_actio| __truncated__ ...
##   .. ..$ : language stop_subscript(class = "vctrs_error_subscript_oob", i = i, subscript_type = subscript_type,      ..., call = call)
##   .. ..$ : language abort(class = c(class, "vctrs_error_subscript"), i = i, ..., call = vctrs_error_call(call))
##   ..$ parent     : int [1:33] 0 1 2 3 4 1 0 0 8 9 ...
##   ..$ visible    : logi [1:33] TRUE TRUE TRUE TRUE TRUE TRUE ...
##   ..$ namespace  : chr [1:33] NA "base" "base" "base" ...
##   ..$ scope      : chr [1:33] "global" "::" "local" "local" ...
##   ..$ error_frame: logi [1:33] FALSE FALSE FALSE FALSE FALSE FALSE ...
##   ..- attr(*, "version")= int 2
##  $ i               : chr "i_love_r"
##  $ subscript_type  : chr "character"
##  $ names           : chr [1:14] "name" "height" "mass" "hair_color" ...
##  $ subscript_action: chr "subset"
##  $ subscript_arg   : chr "x"
##  $ call            : language select(., ...)
##  $ subscript_elt   : chr "column"
##  - attr(*, "class")= chr [1:5] "vctrs_error_subscript_oob" "vctrs_error_subscript" "rlang_error" "error" ...
## Warning in unique_values_safe(starwars, i_love_r): All columns selected!
## Computing on 14 columns...
##       name     height       mass hair_color skin_color  eye_color birth_year 
##         87         46         39         13         31         15         37 
##        sex     gender  homeworld    species      films   vehicles  starships 
##          5          3         49         38         24         11         17

Whether this output is sensible is an open question for the developer. In this case I think it is probably not sensible.

  1. Why might it be a better idea to fail with an error in the previous example instead of continuing with the full data frame?

Engagement

Prompt: Have you tried to catch errors in other languages? If so, how does the condition handling system in R compare? If not, can you think of a more intuitive way to handle errors?