'Using purrr::possibly to catch dynamic error messages

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."


Solution 1:[1]

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)
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1