0

I've written a custom function that does a number of checks and throws a different error when a check fails. Below is a simple example function that takes a data.frame and a column name and simply outputs the sum of that column. I'm using purrr::possibly() to create a saver version of that function so that I can loop over a vector of column names.

foo <- function(df, var){     

  #check 1  
if(var %in% names(df) == FALSE){
    stop(paste0("No column with name ", var, " found."))}  

  #check 2
if(all(is.na(dplyr::select(df, {{var}})))) {
   stop(paste0("All values of column ", var, " are missing."))}
  
  # main function  
  result <- df %>% 
    dplyr::rename(var = {{var}}) %>%
    dplyr::summarise(sum = sum(var))

#print(result) printing shows the correct error messages   
}

safer_foo <- purrr::possibly(.f = foo, otherwise = "error", quiet = FALSE)

I use purrr::map to loop over a vector of columns and store the output in a list. However, for elements where the function fails, I would like to store the specific error message instead of the static input of the "otherwise" argument of purrr::possibly requires. Replacing purrr::possibly with purrr::safely actually captures the specific error message as intended in the $error element of the list but I would like to avoid the extra nested level that safely creates.

test_df <- tibble(A = 1:10, C = NA)
input <- c("A", "B", "C")

output_list <- map(input, ~safer_foo(test_df, .x)) %>% set_names(input)

Output

> output_list

    sum
  <int>
1    55

$B
[1] "error"

$C
[1] "error"

Desired output

> output_list

    sum
  <int>
$A   55

$B
[1] "Error: No column with name B found."

$C
[1] "Error: All values of column C are missing."
Rasul89
  • 588
  • 2
  • 5
  • 14
  • You probably shouldn't be using `stop` in your `foo` function and return the error string directly – Stefano Barbi May 12 '22 at 15:37
  • Do you suggest this because in the case of this example the errors would be fairly clear and stop() is not really necessary? Or is there any negative behavior that stop( ) could introduce if used inside a function? The reason I've opted for the use of stop() was that in my real (more complex) function, I want to execute a number of different checks for the most common problem causes before the main part of the function is even executed. If any of them fails the user should receive a more understandable error message. – Rasul89 May 13 '22 at 09:14

1 Answers1

2

You could tweak purrr::possibly() from its original code to return instead of message the error.

Original code:

## > possibly
## function (.f, otherwise, quiet = TRUE) 
## {
##     .f <- as_mapper(.f)
##     force(otherwise)
##     function(...) {
##         tryCatch(.f(...), error = function(e) {
##             if (!quiet) 
##                 message("Error: ", e$message) ## <--- tweak
##             otherwise
##         }, interrupt = function(e) {
##             stop("Terminated by user", call. = FALSE)
##         })
##     }
## }

tweaked function:

possibly2 <- function (.f, otherwise, quiet = TRUE) {
    .f <- as_mapper(.f)
    force(otherwise)
    function(...) {
        tryCatch(.f(...), error = function(e) {
            if (!quiet) 
                return(e$message) ## <-- tweaked
            otherwise
        }, interrupt = function(e) {
            stop("Terminated by user", call. = FALSE)
        })
    }
}

Example:

safer_foo <- possibly2(.f = foo, otherwise = "error",
                       quiet = FALSE ## don't forget to "unquiet"
                       )

## all other objects / code as in your example

Output:

## > output_list
## $A
## # A tibble: 1 x 1
##     sum
##   <int>
## 1    55
## 
## $B
## [1] "No column with name B found."
## 
## $C
## [1] "All values of column C are missing."

edit

Actually, possibly2 carries over code which is no longer needed. Omitting the unwanted static arguments otherwise and quiet, and skipping the handler for user interrupts, the required code shrinks down to:

possibly2 <- function (.f) {
    .f <- as_mapper(.f)
    function(...) {
        tryCatch(.f(...), error = function(e)  e$message)
    }
}
  • Thank you, this works as intended. I see that object "e" is created inside of the tryCatch( ) function that purrr::possibly calls. But I wonder where the $message part comes from. Where exactly is that message element attached to "e"? Also, if I e.g. wanted to capture warnings instead of errors, I would add the warning = function(w){return(w$message)} argument to tryCatch? Or would that work differently? – Rasul89 May 13 '22 at 09:00
  • The error (a list of class "error") is produced by the instruction that raises the error. It has a list member "message" which you access with `some_error_object$message`. Similar with warning, as you already guessed. You can generate error or warning objects with `simpleError(...)` and `simpleWarning()` and inspect their structure like `simpleWarning(message = "look out!") %>% str` –  May 13 '22 at 10:19