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)
}Conditions
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 from it 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.
- 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.
- Was the error in the previous example thrown by
unique_values()or byselect()? 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 12 31 15 37
sex gender homeworld species films vehicles starships
5 3 49 38 24 11 16
- Why does the warning message show up before the results? Why does the warning message show up before the message, when the line of code with
message()came after the call towarning()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 select columns that don't exist.
✖ Column `i_love_r` doesn't exist.
- 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 11
$ message : chr ""
$ trace :Classes 'rlang_trace', 'rlib_trace', 'tbl' and 'data.frame': 24 obs. of 6 variables:
..$ call :List of 24
.. ..$ : language unique_values_safe(starwars, i_love_r)
.. ..$ : language tryCatch(error = function(cnd) { warning("Attempt to select column has failed") ...
.. ..$ : language tryCatchList(expr, classes, parentenv, handlers)
.. ..$ : language tryCatchOne(expr, names, parentenv, handlers[[1L]])
.. ..$ : language doTryCatch(return(expr), name, parentenv, handler)
.. ..$ : language select(.data, ...)
.. ..$ : language select.data.frame(.data, ...)
.. ..$ : language tidyselect::eval_select(expr(c(...)), data = .data, error_call = error_call)
.. ..$ : language eval_select_impl(data, names(data), as_quosure(expr, env), include = include, exclude = exclude, strict = st| __truncated__ ...
.. ..$ : language with_subscript_errors(out <- vars_select_eval(vars, expr, strict = strict, data = x, name_spec = name_spec, | __truncated__ ...
.. ..$ : language withCallingHandlers(expr, vctrs_error_subscript = function(cnd) { cnd$subscript_action <- subscript_action(type) ...
.. ..$ : 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)
.. ..$ : 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, allow_predicates = allow_predicates, cal| __truncated__
.. ..$ : language as_indices_impl(x, vars, call = call, arg = arg, strict = strict)
.. ..$ : language chr_as_locations(x, vars, call = call, arg = arg)
.. ..$ : language vctrs::vec_as_location(x, n = length(vars), names = vars, call = call, arg = arg)
.. ..$ : 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 = call)
..$ parent : int [1:24] 0 1 2 3 4 1 1 7 8 9 ...
..$ visible : logi [1:24] TRUE TRUE TRUE TRUE TRUE TRUE ...
..$ namespace : chr [1:24] NA "base" "base" "base" ...
..$ scope : chr [1:24] "global" "::" "local" "local" ...
..$ error_frame: logi [1:24] FALSE FALSE FALSE FALSE FALSE FALSE ...
..- attr(*, "version")= int 2
$ parent : NULL
$ i : chr "i_love_r"
$ subscript_type : chr "character"
$ names : chr [1:14] "name" "height" "mass" "hair_color" ...
$ subscript_action: chr "select"
$ subscript_arg : chr "i_love_r"
$ rlang :List of 1
..$ inherit: logi TRUE
$ call : language select(.data, ...)
$ 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 12 31 15 37
sex gender homeworld species films vehicles starships
5 3 49 38 24 11 16
Whether this output is sensible is an open question for the developer. In this case I think it is probably not sensible.
- 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?